@dreamlake/dreamlake-cli 0.1.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.
@@ -0,0 +1,134 @@
1
+ // Shared helpers for project-scoped resource commands (list / create /
2
+ // update / delete of bindrs and datasets). Centralizes project-context
3
+ // resolution, paginated fetches, and glob matching.
4
+ import { HttpError, requestJson } from "./client.js";
5
+ import { resolveNamespace, resolveRemote, resolveToken } from "./config.js";
6
+ import { fail } from "./helpers.js";
7
+ import { matchGlob } from "./glob.js";
8
+ import { parseProject } from "./target.js";
9
+ /** Resolve project + namespace + token, printing the standard errors on failure. */
10
+ export async function resolveProjectCtx(projectStr) {
11
+ let s;
12
+ try {
13
+ s = parseProject(projectStr);
14
+ }
15
+ catch (err) {
16
+ fail(err.message);
17
+ return null;
18
+ }
19
+ const remote = resolveRemote();
20
+ const token = resolveToken();
21
+ if (!token) {
22
+ fail("not authenticated. run 'dreamlake login' first");
23
+ return null;
24
+ }
25
+ const namespace = await resolveNamespace(s.namespace, { token, remote });
26
+ if (!namespace) {
27
+ fail("namespace not specified and no authenticated user found. run 'dreamlake login'");
28
+ return null;
29
+ }
30
+ s.namespace = namespace;
31
+ return { s, token, remote };
32
+ }
33
+ const PAGE_SIZE = 200;
34
+ /** Fetch every page of a `/...?page&pageSize` collection. Returns "not-found" on 404. */
35
+ export async function fetchAllPages(remote, path, token, key) {
36
+ const items = [];
37
+ let page = 1;
38
+ let total = 0;
39
+ for (;;) {
40
+ let data;
41
+ try {
42
+ data = await requestJson(remote, path, {
43
+ token,
44
+ query: { page: String(page), pageSize: String(PAGE_SIZE) },
45
+ });
46
+ }
47
+ catch (err) {
48
+ if (err instanceof HttpError && err.status === 404)
49
+ return "not-found";
50
+ throw err;
51
+ }
52
+ const batch = data[key] ?? [];
53
+ items.push(...batch);
54
+ total = Number(data.total ?? items.length);
55
+ const totalPages = Number(data.totalPages ?? 1);
56
+ if (page >= totalPages || batch.length === 0)
57
+ break;
58
+ page++;
59
+ }
60
+ return { items, total };
61
+ }
62
+ /** Fetch all episodes in a project (throws on error). */
63
+ export async function fetchAllEpisodes(remote, namespace, project, token) {
64
+ const r = await fetchAllPages(remote, `/namespaces/${namespace}/projects/${project}/episodes`, token, "episodes");
65
+ return r === "not-found" ? [] : r.items;
66
+ }
67
+ /** Fetch all bindrs in a project (throws on error). */
68
+ export async function fetchAllBindrs(remote, namespace, project, token) {
69
+ const r = await fetchAllPages(remote, `/namespaces/${namespace}/projects/${project}/bindrs`, token, "bindrs");
70
+ return r === "not-found" ? [] : r.items;
71
+ }
72
+ /** Match episodes by glob against their name OR node path (leading "/" stripped). */
73
+ export function matchEpisodes(episodes, pattern) {
74
+ const pat = pattern.replace(/^\/+/, "");
75
+ return episodes.filter((ep) => matchGlob(ep.name ?? "", pat) ||
76
+ matchGlob((ep.nodePath ?? "").replace(/^\/+/, ""), pat));
77
+ }
78
+ /** Match bindrs by glob against their name. */
79
+ export function matchBindrs(bindrs, pattern) {
80
+ return bindrs.filter((b) => matchGlob(b.name ?? "", pattern));
81
+ }
82
+ /** Fetch all projects in a namespace. */
83
+ export async function fetchProjects(remote, token, namespace) {
84
+ const data = await requestJson(remote, `/namespaces/${namespace}/projects`, { token });
85
+ return data.projects ?? [];
86
+ }
87
+ /** "/a/b" → ",a,b," (comma prefix used by node paths). */
88
+ export function nodePathToCommaPrefix(nodePath) {
89
+ const segs = nodePath.split("/").filter(Boolean);
90
+ return `,${segs.join(",")},`;
91
+ }
92
+ /** Relative display path of a file under a container's comma prefix. */
93
+ export function relPathUnder(file, containerCommaPrefix) {
94
+ const p = file.path ?? "";
95
+ let segs = [];
96
+ if (p.startsWith(containerCommaPrefix)) {
97
+ segs = p.slice(containerCommaPrefix.length).split(",").filter(Boolean);
98
+ }
99
+ return [...segs, file.name].join("/");
100
+ }
101
+ /** Resolve a node by namespace+project+path (+episode) via /nodes/lookup. */
102
+ export async function lookupNode(remote, token, q) {
103
+ return requestJson(remote, "/nodes/lookup", {
104
+ token,
105
+ query: {
106
+ namespace: q.namespace,
107
+ project: q.project,
108
+ path: q.path,
109
+ episode: q.episode ?? undefined,
110
+ },
111
+ });
112
+ }
113
+ /**
114
+ * Fetch every file under a container node via GET /nodes/:id/contents
115
+ * (paginated; ?kind= filters). NOTE: /nodes/:id/descendants exists but its
116
+ * response schema strips the node list on the deployed server — use this.
117
+ */
118
+ export async function fetchAllContents(remote, token, nodeId, kind) {
119
+ const files = [];
120
+ let page = 1;
121
+ for (;;) {
122
+ const data = await requestJson(remote, `/nodes/${nodeId}/contents`, {
123
+ token,
124
+ query: { page, pageSize: 500, kind: kind ?? undefined },
125
+ });
126
+ const batch = data.files ?? [];
127
+ files.push(...batch);
128
+ const pageCount = Number(data.filePageCount ?? 1);
129
+ if (page >= pageCount || batch.length === 0)
130
+ break;
131
+ page++;
132
+ }
133
+ return files;
134
+ }
@@ -0,0 +1,85 @@
1
+ // Target syntax parser — TypeScript port of dreamlake-py's cli/_target.py.
2
+ //
3
+ // Syntax: space[@namespace][:episode][//path]
4
+ //
5
+ // robotics@alice:run-042 → project=robotics, namespace=alice, episode=run-042
6
+ // robotics:run-042 → namespace resolved from current user's token
7
+ // robotics@alice → no episode → project-level
8
+ // robotics → minimal — namespace from token, no episode
9
+ //
10
+ // The `--project` flag uses the shorter space[@namespace] form.
11
+ //
12
+ // NOTE: the Python original (cli/_target.py) has variable-name bugs
13
+ // (`space`/`project`/`nameproject` mixed up). This is the corrected
14
+ // implementation following the documented intent.
15
+ /** Parse a target string: space[@namespace][:episode][//path]. */
16
+ export function parseTarget(target) {
17
+ let rest = target;
18
+ let path = null;
19
+ let episode = null;
20
+ let namespace = null;
21
+ // Split off path (everything after //).
22
+ const slashIdx = rest.indexOf("//");
23
+ if (slashIdx >= 0) {
24
+ path = rest.slice(slashIdx + 2).replace(/^\/+|\/+$/g, "") || null;
25
+ rest = rest.slice(0, slashIdx);
26
+ }
27
+ // Split off episode (everything after the first :).
28
+ const colonIdx = rest.indexOf(":");
29
+ let base;
30
+ if (colonIdx >= 0) {
31
+ base = rest.slice(0, colonIdx);
32
+ episode = rest.slice(colonIdx + 1) || null;
33
+ }
34
+ else {
35
+ base = rest;
36
+ }
37
+ // Split off namespace (everything after @).
38
+ const atIdx = base.indexOf("@");
39
+ let project;
40
+ if (atIdx >= 0) {
41
+ project = base.slice(0, atIdx);
42
+ namespace = base.slice(atIdx + 1) || null;
43
+ }
44
+ else {
45
+ project = base;
46
+ }
47
+ if (!project) {
48
+ throw new Error("target must include a project name");
49
+ }
50
+ return { namespace, project, episode, path };
51
+ }
52
+ /** Parse a space target: space[@namespace]. */
53
+ export function parseProject(target) {
54
+ const atIdx = target.indexOf("@");
55
+ let project;
56
+ let namespace = null;
57
+ if (atIdx >= 0) {
58
+ project = target.slice(0, atIdx);
59
+ namespace = target.slice(atIdx + 1) || null;
60
+ }
61
+ else {
62
+ project = target;
63
+ }
64
+ if (!project) {
65
+ throw new Error("target must include a project name");
66
+ }
67
+ return { namespace, project };
68
+ }
69
+ /** Reconstruct a target string from parsed components (for display). */
70
+ export function formatTarget(t) {
71
+ let parts = t.project;
72
+ if (t.namespace)
73
+ parts += `@${t.namespace}`;
74
+ if (t.episode)
75
+ parts += `:${t.episode}`;
76
+ if (t.path)
77
+ parts += `//${t.path}`;
78
+ return parts;
79
+ }
80
+ /** Reconstruct a project string for display. */
81
+ export function formatProject(s) {
82
+ if (s.namespace)
83
+ return `${s.project}@${s.namespace}`;
84
+ return s.project;
85
+ }
@@ -0,0 +1,411 @@
1
+ // `dreamlake team ...` — team management within an organization.
2
+ // GraphQL-backed (see ../graphql.ts). Teams are scoped to an org and
3
+ // identified by (org-slug, team-slug); the CLI resolves them to ids.
4
+ import { gql } from "../graphql.js";
5
+ import { resolveOrg, resolveTeam, resolveUserId } from "../graphql-helpers.js";
6
+ import { emitJson, fail, ok, renderTable } from "../helpers.js";
7
+ import { confirm } from "../prompt.js";
8
+ function normalizeRole(raw) {
9
+ if (!raw)
10
+ return undefined;
11
+ const v = raw.toLowerCase();
12
+ if (v === "maintainer")
13
+ return "MAINTAINER";
14
+ if (v === "member")
15
+ return "MEMBER";
16
+ throw new Error(`--role must be 'maintainer' or 'member', got '${raw}'`);
17
+ }
18
+ function normalizeVisibility(raw) {
19
+ if (!raw)
20
+ return undefined;
21
+ const v = raw.toLowerCase();
22
+ if (v === "visible")
23
+ return "VISIBLE";
24
+ if (v === "secret")
25
+ return "SECRET";
26
+ throw new Error(`--visibility must be 'visible' or 'secret', got '${raw}'`);
27
+ }
28
+ // ─── list ────────────────────────────────────────────────────────────
29
+ export async function runTeamList(orgSlug, opts) {
30
+ try {
31
+ let teams;
32
+ if (orgSlug) {
33
+ const org = await resolveOrg(orgSlug);
34
+ if (!org) {
35
+ fail(`organization '${orgSlug}' not found`);
36
+ return 1;
37
+ }
38
+ const data = await gql(`query($orgId:ID!){ organizationTeams(orgId:$orgId){ slug name visibility } }`, { orgId: org.id });
39
+ teams = data.organizationTeams ?? [];
40
+ }
41
+ else {
42
+ const data = await gql(`query{ myTeams{ slug name visibility org{ slug } } }`);
43
+ teams = data.myTeams ?? [];
44
+ }
45
+ if (opts.json) {
46
+ emitJson(teams);
47
+ return 0;
48
+ }
49
+ process.stdout.write(orgSlug ? `Teams in ${orgSlug}\n\n` : "My teams\n\n");
50
+ if (teams.length === 0) {
51
+ process.stdout.write(" (none)\n");
52
+ return 0;
53
+ }
54
+ const rows = teams.map((t) => ({
55
+ slug: t.slug,
56
+ name: t.name,
57
+ visibility: t.visibility ?? "",
58
+ ...(orgSlug ? {} : { org: t.org?.slug ?? "" }),
59
+ }));
60
+ const cols = orgSlug ? ["slug", "name", "visibility"] : ["slug", "name", "visibility", "org"];
61
+ process.stdout.write(renderTable(rows, cols));
62
+ process.stdout.write(`\n ${teams.length} team(s)\n`);
63
+ return 0;
64
+ }
65
+ catch (err) {
66
+ fail(err.message);
67
+ return 1;
68
+ }
69
+ }
70
+ // ─── show ────────────────────────────────────────────────────────────
71
+ export async function runTeamShow(orgSlug, teamSlug, opts) {
72
+ try {
73
+ const ref = await resolveTeam(orgSlug, teamSlug);
74
+ if (!ref) {
75
+ fail(`team '${teamSlug}' not found in ${orgSlug}`);
76
+ return 1;
77
+ }
78
+ const data = await gql(`query($id:ID!){ team(id:$id){
79
+ slug name description visibility myRole
80
+ parent{ slug name } children{ slug name }
81
+ members{ role user{ name email slug } }
82
+ } }`, { id: ref.id });
83
+ const team = data.team;
84
+ if (!team) {
85
+ fail(`team '${teamSlug}' not found`);
86
+ return 1;
87
+ }
88
+ if (opts.json) {
89
+ emitJson(team);
90
+ return 0;
91
+ }
92
+ process.stdout.write(`Team: ${team.name} (${orgSlug}/${team.slug})\n`);
93
+ process.stdout.write(` visibility: ${team.visibility ?? ""}\n`);
94
+ process.stdout.write(` your role: ${team.myRole ?? "(not a member)"}\n`);
95
+ if (team.description)
96
+ process.stdout.write(` description: ${team.description}\n`);
97
+ if (team.parent)
98
+ process.stdout.write(` parent: ${team.parent.slug}\n`);
99
+ if (team.children?.length) {
100
+ process.stdout.write(` children: ${team.children.map((c) => c.slug).join(", ")}\n`);
101
+ }
102
+ process.stdout.write(`\n Members (${team.members?.length ?? 0}):\n`);
103
+ const rows = (team.members ?? []).map((m) => ({
104
+ role: m.role,
105
+ name: m.user?.name ?? "",
106
+ email: m.user?.email ?? "",
107
+ namespace: m.user?.slug ?? "",
108
+ }));
109
+ process.stdout.write(rows.length ? renderTable(rows, ["role", "name", "email", "namespace"]) : " (none)\n");
110
+ return 0;
111
+ }
112
+ catch (err) {
113
+ fail(err.message);
114
+ return 1;
115
+ }
116
+ }
117
+ // ─── create / update / delete / leave ────────────────────────────────
118
+ export async function runTeamCreate(orgSlug, teamSlug, opts) {
119
+ try {
120
+ const visibility = normalizeVisibility(opts.visibility);
121
+ const org = await resolveOrg(orgSlug);
122
+ if (!org) {
123
+ fail(`organization '${orgSlug}' not found`);
124
+ return 1;
125
+ }
126
+ let parentId;
127
+ if (opts.parent) {
128
+ const parent = await resolveTeam(orgSlug, opts.parent);
129
+ if (!parent) {
130
+ fail(`parent team '${opts.parent}' not found in ${orgSlug}`);
131
+ return 1;
132
+ }
133
+ parentId = parent.id;
134
+ }
135
+ const input = {
136
+ orgId: org.id,
137
+ slug: teamSlug,
138
+ name: opts.name ?? teamSlug,
139
+ };
140
+ if (opts.description)
141
+ input.description = opts.description;
142
+ if (visibility)
143
+ input.visibility = visibility;
144
+ if (parentId)
145
+ input.parentId = parentId;
146
+ await gql(`mutation($input:TeamCreateInput!){ teamCreate(input:$input){ id slug } }`, { input });
147
+ ok(`Created team: ${teamSlug} in ${orgSlug}`);
148
+ process.stdout.write(` you are now MAINTAINER\n`);
149
+ if (opts.parent)
150
+ process.stdout.write(` parent: ${opts.parent}\n`);
151
+ return 0;
152
+ }
153
+ catch (err) {
154
+ fail(err.message);
155
+ return 1;
156
+ }
157
+ }
158
+ export async function runTeamUpdate(orgSlug, teamSlug, opts) {
159
+ try {
160
+ const visibility = normalizeVisibility(opts.visibility);
161
+ if (opts.name === undefined && opts.description === undefined && visibility === undefined) {
162
+ fail("nothing to update. use --name, --description, or --visibility");
163
+ return 1;
164
+ }
165
+ const ref = await resolveTeam(orgSlug, teamSlug);
166
+ if (!ref) {
167
+ fail(`team '${teamSlug}' not found in ${orgSlug}`);
168
+ return 1;
169
+ }
170
+ const input = {};
171
+ if (opts.name !== undefined)
172
+ input.name = opts.name;
173
+ if (opts.description !== undefined)
174
+ input.description = opts.description;
175
+ if (visibility !== undefined)
176
+ input.visibility = visibility;
177
+ await gql(`mutation($id:ID!,$input:TeamUpdateInput!){ teamUpdate(id:$id, input:$input){ id } }`, { id: ref.id, input });
178
+ ok(`Updated team: ${orgSlug}/${teamSlug}`);
179
+ return 0;
180
+ }
181
+ catch (err) {
182
+ fail(err.message);
183
+ return 1;
184
+ }
185
+ }
186
+ export async function runTeamDelete(orgSlug, teamSlug, opts) {
187
+ try {
188
+ const ref = await resolveTeam(orgSlug, teamSlug);
189
+ if (!ref) {
190
+ fail(`team '${teamSlug}' not found in ${orgSlug}`);
191
+ return 1;
192
+ }
193
+ if (!opts.yes) {
194
+ const proceed = await confirm(`Delete team '${orgSlug}/${teamSlug}'? (must have no child teams)`, false);
195
+ if (!proceed) {
196
+ process.stdout.write("Cancelled.\n");
197
+ return 0;
198
+ }
199
+ }
200
+ await gql(`mutation($id:ID!){ teamDelete(id:$id) }`, { id: ref.id });
201
+ ok(`Deleted team: ${orgSlug}/${teamSlug}`);
202
+ return 0;
203
+ }
204
+ catch (err) {
205
+ fail(err.message);
206
+ return 1;
207
+ }
208
+ }
209
+ export async function runTeamLeave(orgSlug, teamSlug, opts) {
210
+ try {
211
+ const ref = await resolveTeam(orgSlug, teamSlug);
212
+ if (!ref) {
213
+ fail(`team '${teamSlug}' not found in ${orgSlug}`);
214
+ return 1;
215
+ }
216
+ if (!opts.yes) {
217
+ const proceed = await confirm(`Leave team '${orgSlug}/${teamSlug}'?`, false);
218
+ if (!proceed) {
219
+ process.stdout.write("Cancelled.\n");
220
+ return 0;
221
+ }
222
+ }
223
+ await gql(`mutation($id:ID!){ teamMemberLeave(teamId:$id) }`, { id: ref.id });
224
+ ok(`Left team: ${orgSlug}/${teamSlug}`);
225
+ return 0;
226
+ }
227
+ catch (err) {
228
+ fail(err.message);
229
+ return 1;
230
+ }
231
+ }
232
+ // ─── member subcommands ──────────────────────────────────────────────
233
+ export async function runTeamMemberList(orgSlug, teamSlug, opts) {
234
+ try {
235
+ const ref = await resolveTeam(orgSlug, teamSlug);
236
+ if (!ref) {
237
+ fail(`team '${teamSlug}' not found in ${orgSlug}`);
238
+ return 1;
239
+ }
240
+ const data = await gql(`query($id:ID!){ teamMembers(teamId:$id){ role joinedAt user{ name email slug } } }`, { id: ref.id });
241
+ const members = data.teamMembers ?? [];
242
+ if (opts.json) {
243
+ emitJson(members);
244
+ return 0;
245
+ }
246
+ process.stdout.write(`Members of ${orgSlug}/${teamSlug}\n\n`);
247
+ const rows = members.map((m) => ({
248
+ role: m.role,
249
+ name: m.user?.name ?? "",
250
+ email: m.user?.email ?? "",
251
+ namespace: m.user?.slug ?? "",
252
+ joined: String(m.joinedAt ?? "").slice(0, 10),
253
+ }));
254
+ process.stdout.write(rows.length ? renderTable(rows, ["role", "name", "email", "namespace", "joined"]) : " (none)\n");
255
+ process.stdout.write(`\n ${members.length} member(s)\n`);
256
+ return 0;
257
+ }
258
+ catch (err) {
259
+ fail(err.message);
260
+ return 1;
261
+ }
262
+ }
263
+ export async function runTeamMemberAdd(orgSlug, teamSlug, user, opts) {
264
+ try {
265
+ const role = normalizeRole(opts.role);
266
+ const ref = await resolveTeam(orgSlug, teamSlug);
267
+ if (!ref) {
268
+ fail(`team '${teamSlug}' not found in ${orgSlug}`);
269
+ return 1;
270
+ }
271
+ const userId = await resolveUserId(user);
272
+ await gql(`mutation($teamId:ID!,$userId:ID!,$role:TeamRole){
273
+ teamMemberAdd(teamId:$teamId, userId:$userId, role:$role){ id } }`, { teamId: ref.id, userId, role: role ?? null });
274
+ ok(`Added ${user} to ${orgSlug}/${teamSlug} as ${role ?? "MEMBER"}`);
275
+ return 0;
276
+ }
277
+ catch (err) {
278
+ fail(err.message);
279
+ return 1;
280
+ }
281
+ }
282
+ export async function runTeamMemberRole(orgSlug, teamSlug, user, opts) {
283
+ try {
284
+ const role = normalizeRole(opts.role);
285
+ if (!role) {
286
+ fail("--role is required (maintainer|member)");
287
+ return 1;
288
+ }
289
+ const ref = await resolveTeam(orgSlug, teamSlug);
290
+ if (!ref) {
291
+ fail(`team '${teamSlug}' not found in ${orgSlug}`);
292
+ return 1;
293
+ }
294
+ const userId = await resolveUserId(user);
295
+ await gql(`mutation($teamId:ID!,$userId:ID!,$role:TeamRole!){
296
+ teamMemberUpdateRole(teamId:$teamId, userId:$userId, role:$role){ id } }`, { teamId: ref.id, userId, role });
297
+ ok(`Set ${user}'s role in ${orgSlug}/${teamSlug} to ${role}`);
298
+ return 0;
299
+ }
300
+ catch (err) {
301
+ fail(err.message);
302
+ return 1;
303
+ }
304
+ }
305
+ export async function runTeamMemberRemove(orgSlug, teamSlug, user, opts) {
306
+ try {
307
+ const ref = await resolveTeam(orgSlug, teamSlug);
308
+ if (!ref) {
309
+ fail(`team '${teamSlug}' not found in ${orgSlug}`);
310
+ return 1;
311
+ }
312
+ const userId = await resolveUserId(user);
313
+ if (!opts.yes) {
314
+ const proceed = await confirm(`Remove ${user} from ${orgSlug}/${teamSlug}?`, false);
315
+ if (!proceed) {
316
+ process.stdout.write("Cancelled.\n");
317
+ return 0;
318
+ }
319
+ }
320
+ await gql(`mutation($teamId:ID!,$userId:ID!){ teamMemberRemove(teamId:$teamId, userId:$userId) }`, { teamId: ref.id, userId });
321
+ ok(`Removed ${user} from ${orgSlug}/${teamSlug}`);
322
+ return 0;
323
+ }
324
+ catch (err) {
325
+ fail(err.message);
326
+ return 1;
327
+ }
328
+ }
329
+ // ─── registration ────────────────────────────────────────────────────
330
+ export function registerTeamCommand(program) {
331
+ const team = program.command("team").description("manage teams within an organization");
332
+ team
333
+ .command("list")
334
+ .description("list teams in an org, or all teams you belong to (omit <org>)")
335
+ .argument("[org]", "organization slug (omit to list your own teams)")
336
+ .option("--json", "emit JSON")
337
+ .action(async (org, opts) => process.exit(await runTeamList(org, opts)));
338
+ team
339
+ .command("show")
340
+ .description("show a team with its members and sub-teams")
341
+ .argument("<org>", "organization slug")
342
+ .argument("<team>", "team slug")
343
+ .option("--json", "emit JSON")
344
+ .action(async (org, t, opts) => process.exit(await runTeamShow(org, t, opts)));
345
+ team
346
+ .command("create")
347
+ .description("create a team in an organization (you become MAINTAINER)")
348
+ .argument("<org>", "organization slug")
349
+ .argument("<team>", "team slug (3-40 chars, lowercase/hyphens)")
350
+ .option("--name <text>", "display name (default: slug)")
351
+ .option("--description <text>", "description")
352
+ .option("--visibility <vis>", "visible|secret (default: visible)")
353
+ .option("--parent <team>", "parent team slug (for nesting)")
354
+ .action(async (org, t, opts) => process.exit(await runTeamCreate(org, t, opts)));
355
+ team
356
+ .command("update")
357
+ .description("update a team's name / description / visibility")
358
+ .argument("<org>", "organization slug")
359
+ .argument("<team>", "team slug")
360
+ .option("--name <text>", "new display name")
361
+ .option("--description <text>", "new description")
362
+ .option("--visibility <vis>", "visible|secret")
363
+ .action(async (org, t, opts) => process.exit(await runTeamUpdate(org, t, opts)));
364
+ team
365
+ .command("delete")
366
+ .description("delete a team (must have no child teams)")
367
+ .argument("<org>", "organization slug")
368
+ .argument("<team>", "team slug")
369
+ .option("--yes", "skip the confirmation prompt")
370
+ .action(async (org, t, opts) => process.exit(await runTeamDelete(org, t, opts)));
371
+ team
372
+ .command("leave")
373
+ .description("leave a team (last maintainer must promote someone first)")
374
+ .argument("<org>", "organization slug")
375
+ .argument("<team>", "team slug")
376
+ .option("--yes", "skip the confirmation prompt")
377
+ .action(async (org, t, opts) => process.exit(await runTeamLeave(org, t, opts)));
378
+ // ── members ──
379
+ const member = team.command("member").description("manage team members");
380
+ member
381
+ .command("list")
382
+ .description("list members of a team")
383
+ .argument("<org>", "organization slug")
384
+ .argument("<team>", "team slug")
385
+ .option("--json", "emit JSON")
386
+ .action(async (org, t, opts) => process.exit(await runTeamMemberList(org, t, opts)));
387
+ member
388
+ .command("add")
389
+ .description("add a user to a team (added to the org too if needed)")
390
+ .argument("<org>", "organization slug")
391
+ .argument("<team>", "team slug")
392
+ .argument("<user>", "user email or name")
393
+ .option("--role <role>", "maintainer|member (default: member)")
394
+ .action(async (org, t, user, opts) => process.exit(await runTeamMemberAdd(org, t, user, opts)));
395
+ member
396
+ .command("role")
397
+ .description("change a team member's role")
398
+ .argument("<org>", "organization slug")
399
+ .argument("<team>", "team slug")
400
+ .argument("<user>", "user email or name")
401
+ .requiredOption("--role <role>", "maintainer|member")
402
+ .action(async (org, t, user, opts) => process.exit(await runTeamMemberRole(org, t, user, opts)));
403
+ member
404
+ .command("remove")
405
+ .description("remove a member from a team")
406
+ .argument("<org>", "organization slug")
407
+ .argument("<team>", "team slug")
408
+ .argument("<user>", "user email or name")
409
+ .option("--yes", "skip the confirmation prompt")
410
+ .action(async (org, t, user, opts) => process.exit(await runTeamMemberRemove(org, t, user, opts)));
411
+ }