@neta-art/cohub-cli 3.2.0 → 3.3.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 CHANGED
@@ -65,6 +65,9 @@ COHUB_SPACE_ID=<spaceId> cohub spaces get
65
65
  cohub spaces create --name "<name>" --description "<description>" --json
66
66
  cohub spaces update <spaceId> --slug <space-slug>
67
67
  cohub spaces rename <spaceId> "<new name>"
68
+ cohub -s <spaceId> spaces invites create --role builder --days 7
69
+ cohub -s <spaceId> spaces invites ls
70
+ cohub -s <spaceId> spaces invites revoke <code> --yes
68
71
  cohub -s <spaceId> run -- git status
69
72
  ```
70
73
 
@@ -0,0 +1,28 @@
1
+ import { type CreateInvitationInput, type CreateInvitationResponse, type SpaceInvitationListResponse } from "@neta-art/cohub";
2
+ import type { Command } from "commander";
3
+ export type SpaceInvitationCreateCliOptions = {
4
+ role?: string;
5
+ days?: string;
6
+ maxUses?: string;
7
+ json?: boolean;
8
+ };
9
+ type SpaceInvitationCommandClient = {
10
+ space(spaceId: string): {
11
+ invitations: {
12
+ list(): Promise<SpaceInvitationListResponse>;
13
+ create(input: CreateInvitationInput): Promise<CreateInvitationResponse>;
14
+ revoke(token: string): Promise<{
15
+ ok: true;
16
+ }>;
17
+ };
18
+ };
19
+ };
20
+ export declare class InvalidSpaceInvitationCliOptionsError extends Error {
21
+ readonly detail: string;
22
+ constructor(message: string, detail: string);
23
+ }
24
+ export declare function parseSpaceInvitationCreateOptions(options: SpaceInvitationCreateCliOptions): Required<Pick<CreateInvitationInput, "role" | "ttlSeconds" | "maxUses">>;
25
+ export declare function registerSpaceInvitations(spacesCommand: Command, dependencies?: {
26
+ createClient: () => SpaceInvitationCommandClient;
27
+ }): Command;
28
+ export {};
@@ -0,0 +1,168 @@
1
+ import { buildSpaceInvitePath, } from "@neta-art/cohub";
2
+ import { createClient } from "../client.js";
3
+ import { error, handleHttp, json as outJson, jsonRequested, ok, table, } from "../output.js";
4
+ import { resolveSpace } from "../space.js";
5
+ const SPACE_ROLES = ["host", "builder", "guest"];
6
+ const DEFAULT_DAYS = 7;
7
+ const MAX_DAYS = 30;
8
+ const MAX_USES = 10_000;
9
+ export class InvalidSpaceInvitationCliOptionsError extends Error {
10
+ detail;
11
+ constructor(message, detail) {
12
+ super(message);
13
+ this.detail = detail;
14
+ this.name = "InvalidSpaceInvitationCliOptionsError";
15
+ }
16
+ }
17
+ function parseInteger(value, label, min, max) {
18
+ if (!/^\d+$/.test(value.trim())) {
19
+ throw new InvalidSpaceInvitationCliOptionsError(`Invalid ${label}`, `${label} must be an integer from ${min} to ${max}`);
20
+ }
21
+ const parsed = Number.parseInt(value, 10);
22
+ if (!Number.isSafeInteger(parsed) || parsed < min || parsed > max) {
23
+ throw new InvalidSpaceInvitationCliOptionsError(`Invalid ${label}`, `${label} must be an integer from ${min} to ${max}`);
24
+ }
25
+ return parsed;
26
+ }
27
+ export function parseSpaceInvitationCreateOptions(options) {
28
+ const role = (options.role ?? "builder");
29
+ if (!SPACE_ROLES.includes(role)) {
30
+ throw new InvalidSpaceInvitationCliOptionsError("Invalid role", `Use one of: ${SPACE_ROLES.join(", ")}`);
31
+ }
32
+ const days = parseInteger(options.days ?? String(DEFAULT_DAYS), "days", 1, MAX_DAYS);
33
+ const maxUses = parseInteger(options.maxUses ?? "0", "max uses", 0, MAX_USES);
34
+ return { role, ttlSeconds: days * 24 * 60 * 60, maxUses };
35
+ }
36
+ function invitationUrl(invitation) {
37
+ const origin = process.env.COHUB_WEB_URL?.replace(/\/+$/, "") ?? "https://cohub.run";
38
+ return `${origin}${buildSpaceInvitePath({
39
+ spaceId: invitation.spaceId,
40
+ ownerUsername: invitation.ownerUsername,
41
+ spaceSlug: invitation.spaceSlug,
42
+ inviteCode: invitation.token,
43
+ })}`;
44
+ }
45
+ async function confirmRevoke(options) {
46
+ if (options.yes)
47
+ return;
48
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
49
+ return error("Confirmation required", "Pass --yes to revoke the invite link.");
50
+ }
51
+ process.stdout.write("This invite link will stop working. Continue? [y/N] ");
52
+ const chunks = [];
53
+ for await (const chunk of process.stdin) {
54
+ chunks.push(chunk);
55
+ break;
56
+ }
57
+ const answer = Buffer.concat(chunks).toString().trim().toLowerCase();
58
+ if (answer !== "y" && answer !== "yes")
59
+ return error("Cancelled");
60
+ }
61
+ export function registerSpaceInvitations(spacesCommand, dependencies = {
62
+ createClient,
63
+ }) {
64
+ const invitations = spacesCommand
65
+ .command("invites")
66
+ .description("Create and manage space invite links");
67
+ invitations
68
+ .command("create")
69
+ .description("Create an invite link")
70
+ .option("--role <role>", "Member role: host, builder, or guest", "builder")
71
+ .option("--days <days>", "Validity in days, from 1 to 30", String(DEFAULT_DAYS))
72
+ .option("--max-uses <count>", "Usage limit, or 0 for unlimited", "0")
73
+ .option("--json", "Output as JSON")
74
+ .action(async (options) => {
75
+ const spaceId = resolveSpace(spacesCommand);
76
+ let input;
77
+ try {
78
+ input = parseSpaceInvitationCreateOptions(options);
79
+ }
80
+ catch (cause) {
81
+ if (cause instanceof InvalidSpaceInvitationCliOptionsError) {
82
+ return error(cause.message, cause.detail);
83
+ }
84
+ throw cause;
85
+ }
86
+ try {
87
+ const created = await dependencies
88
+ .createClient()
89
+ .space(spaceId)
90
+ .invitations.create(input);
91
+ const output = {
92
+ ...created,
93
+ url: invitationUrl({
94
+ ...created,
95
+ spaceId: created.spaceId || spaceId,
96
+ ownerUsername: created.ownerUsername ?? null,
97
+ spaceSlug: created.spaceSlug ?? null,
98
+ }),
99
+ };
100
+ if (jsonRequested(options))
101
+ return outJson(output);
102
+ ok(`Invite link created: ${output.url}`);
103
+ }
104
+ catch (cause) {
105
+ handleHttp(cause);
106
+ }
107
+ });
108
+ invitations
109
+ .command("ls")
110
+ .alias("list")
111
+ .description("List invite links")
112
+ .option("--json", "Output as JSON")
113
+ .action(async (options) => {
114
+ const spaceId = resolveSpace(spacesCommand);
115
+ try {
116
+ const result = await dependencies
117
+ .createClient()
118
+ .space(spaceId)
119
+ .invitations.list();
120
+ const items = result.items.map((item) => ({
121
+ ...item,
122
+ url: invitationUrl({
123
+ ...result,
124
+ token: item.token,
125
+ spaceId: result.spaceId || spaceId,
126
+ ownerUsername: result.ownerUsername ?? null,
127
+ spaceSlug: result.spaceSlug ?? null,
128
+ }),
129
+ uses: item.maxUses ? `${item.useCount}/${item.maxUses}` : String(item.useCount),
130
+ }));
131
+ if (jsonRequested(options))
132
+ return outJson({ ...result, items });
133
+ table(items, [
134
+ { key: "token", label: "Code" },
135
+ { key: "role", label: "Role" },
136
+ { key: "status", label: "Status" },
137
+ { key: "uses", label: "Uses" },
138
+ { key: "expiresInSeconds", label: "Expires in" },
139
+ { key: "url", label: "URL" },
140
+ ]);
141
+ }
142
+ catch (cause) {
143
+ handleHttp(cause);
144
+ }
145
+ });
146
+ invitations
147
+ .command("revoke <code>")
148
+ .description("Revoke an invite link")
149
+ .option("-y, --yes", "Confirm revocation")
150
+ .option("--json", "Output as JSON")
151
+ .action(async (code, options) => {
152
+ await confirmRevoke(options);
153
+ const spaceId = resolveSpace(spacesCommand);
154
+ try {
155
+ const result = await dependencies
156
+ .createClient()
157
+ .space(spaceId)
158
+ .invitations.revoke(code);
159
+ if (jsonRequested(options))
160
+ return outJson(result);
161
+ ok("Invite link revoked");
162
+ }
163
+ catch (cause) {
164
+ handleHttp(cause);
165
+ }
166
+ });
167
+ return invitations;
168
+ }
@@ -8,6 +8,7 @@ import { createClient } from "../client.js";
8
8
  import { table, json as outJson, jsonRequested, ok, error, handleHttp } from "../output.js";
9
9
  import { resolveSpace } from "../space.js";
10
10
  import { registerSpaceCommerce } from "./space-commerce.js";
11
+ import { registerSpaceInvitations } from "./space-invitations.js";
11
12
  import { registerSpaceTurns } from "./space-turns.js";
12
13
  const cliEnv = resolveCohubEnvironment();
13
14
  const defaultIdleTtlSeconds = cliEnv === "prod" ? 12 * 60 * 60 : 10 * 60;
@@ -377,6 +378,7 @@ export function registerPrompt(program) {
377
378
  }
378
379
  export function registerSpaces(program) {
379
380
  const spacesCmd = program.command("spaces").description("Space management");
381
+ registerSpaceInvitations(spacesCmd);
380
382
  // ── spaces ls ──
381
383
  spacesCmd
382
384
  .command("ls")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub-cli",
3
- "version": "3.2.0",
3
+ "version": "3.3.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": "4.4.0"
22
+ "@neta-art/cohub": "4.5.0"
23
23
  },
24
24
  "publishConfig": {
25
25
  "access": "public"