@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,348 @@
1
+ // `dreamlake org ...` — organization management. Backed by the
2
+ // dreamlake-server GraphQL endpoint (see ../graphql.ts).
3
+ //
4
+ // Commands take slugs (and email/name for users); ids are resolved
5
+ // internally. Roles are accepted case-insensitively as owner|member and
6
+ // sent as the OWNER|MEMBER enum.
7
+ import { gql } from "../graphql.js";
8
+ import { resolveOrg, resolveUserId } from "../graphql-helpers.js";
9
+ import { emitJson, fail, ok, renderTable } from "../helpers.js";
10
+ import { confirm } from "../prompt.js";
11
+ function normalizeRole(raw) {
12
+ if (!raw)
13
+ return undefined;
14
+ const v = raw.toLowerCase();
15
+ if (v === "owner")
16
+ return "OWNER";
17
+ if (v === "member")
18
+ return "MEMBER";
19
+ throw new Error(`--role must be 'owner' or 'member', got '${raw}'`);
20
+ }
21
+ // ─── commands ────────────────────────────────────────────────────────
22
+ export async function runOrgList(opts) {
23
+ try {
24
+ const data = await gql(`query{ myOrganizations{ slug name myRole description } }`);
25
+ const orgs = data.myOrganizations ?? [];
26
+ if (opts.json) {
27
+ emitJson(orgs);
28
+ return 0;
29
+ }
30
+ process.stdout.write("My organizations\n\n");
31
+ if (orgs.length === 0) {
32
+ process.stdout.write(" (none)\n");
33
+ return 0;
34
+ }
35
+ const rows = orgs.map((o) => ({
36
+ slug: o.slug,
37
+ name: o.name,
38
+ role: o.myRole ?? "",
39
+ description: (o.description ?? "").slice(0, 40),
40
+ }));
41
+ process.stdout.write(renderTable(rows, ["slug", "name", "role", "description"]));
42
+ process.stdout.write(`\n ${orgs.length} organization(s)\n`);
43
+ return 0;
44
+ }
45
+ catch (err) {
46
+ fail(err.message);
47
+ return 1;
48
+ }
49
+ }
50
+ export async function runOrgShow(slug, opts) {
51
+ try {
52
+ const data = await gql(`query($slug:String!){ organizationBySlug(slug:$slug){
53
+ id slug name description myRole
54
+ members{ role user{ name email slug } }
55
+ teams{ slug name visibility }
56
+ } }`, { slug });
57
+ const org = data.organizationBySlug;
58
+ if (!org) {
59
+ fail(`organization '${slug}' not found`);
60
+ return 1;
61
+ }
62
+ if (opts.json) {
63
+ emitJson(org);
64
+ return 0;
65
+ }
66
+ process.stdout.write(`Organization: ${org.name} (${org.slug})\n`);
67
+ process.stdout.write(` your role: ${org.myRole ?? "(not a member)"}\n`);
68
+ if (org.description)
69
+ process.stdout.write(` description: ${org.description}\n`);
70
+ process.stdout.write(`\n Members (${org.members?.length ?? 0}):\n`);
71
+ const mrows = (org.members ?? []).map((m) => ({
72
+ role: m.role,
73
+ name: m.user?.name ?? "",
74
+ email: m.user?.email ?? "",
75
+ namespace: m.user?.slug ?? "",
76
+ }));
77
+ process.stdout.write(mrows.length ? renderTable(mrows, ["role", "name", "email", "namespace"]) : " (none)\n");
78
+ process.stdout.write(`\n Teams (${org.teams?.length ?? 0}):\n`);
79
+ const trows = (org.teams ?? []).map((t) => ({
80
+ slug: t.slug,
81
+ name: t.name,
82
+ visibility: t.visibility ?? "",
83
+ }));
84
+ process.stdout.write(trows.length ? renderTable(trows, ["slug", "name", "visibility"]) : " (none)\n");
85
+ return 0;
86
+ }
87
+ catch (err) {
88
+ fail(err.message);
89
+ return 1;
90
+ }
91
+ }
92
+ export async function runOrgCreate(slug, opts) {
93
+ try {
94
+ const input = { slug, name: opts.name ?? slug };
95
+ if (opts.description)
96
+ input.description = opts.description;
97
+ const data = await gql(`mutation($input:OrganizationCreateInput!){
98
+ organizationCreate(input:$input){ id slug name } }`, { input });
99
+ const org = data.organizationCreate;
100
+ ok(`Created organization: ${org.name} (${org.slug})`);
101
+ process.stdout.write(` you are now OWNER\n`);
102
+ process.stdout.write(` a namespace '${org.slug}' was created for it\n`);
103
+ return 0;
104
+ }
105
+ catch (err) {
106
+ fail(err.message);
107
+ return 1;
108
+ }
109
+ }
110
+ export async function runOrgUpdate(slug, opts) {
111
+ if (opts.name === undefined && opts.description === undefined) {
112
+ fail("nothing to update. use --name or --description");
113
+ return 1;
114
+ }
115
+ try {
116
+ const org = await resolveOrg(slug);
117
+ if (!org) {
118
+ fail(`organization '${slug}' not found`);
119
+ return 1;
120
+ }
121
+ const input = {};
122
+ if (opts.name !== undefined)
123
+ input.name = opts.name;
124
+ if (opts.description !== undefined)
125
+ input.description = opts.description;
126
+ await gql(`mutation($id:ID!,$input:OrganizationUpdateInput!){
127
+ organizationUpdate(id:$id, input:$input){ id } }`, { id: org.id, input });
128
+ ok(`Updated organization: ${slug}`);
129
+ return 0;
130
+ }
131
+ catch (err) {
132
+ fail(err.message);
133
+ return 1;
134
+ }
135
+ }
136
+ export async function runOrgDelete(slug, opts) {
137
+ try {
138
+ const org = await resolveOrg(slug);
139
+ if (!org) {
140
+ fail(`organization '${slug}' not found`);
141
+ return 1;
142
+ }
143
+ if (!opts.yes) {
144
+ const proceed = await confirm(`Delete organization '${slug}'? (requires no remaining projects)`, false);
145
+ if (!proceed) {
146
+ process.stdout.write("Cancelled.\n");
147
+ return 0;
148
+ }
149
+ }
150
+ await gql(`mutation($id:ID!){ organizationDelete(id:$id) }`, { id: org.id });
151
+ ok(`Deleted organization: ${slug}`);
152
+ return 0;
153
+ }
154
+ catch (err) {
155
+ fail(err.message);
156
+ return 1;
157
+ }
158
+ }
159
+ export async function runOrgLeave(slug, opts) {
160
+ try {
161
+ const org = await resolveOrg(slug);
162
+ if (!org) {
163
+ fail(`organization '${slug}' not found`);
164
+ return 1;
165
+ }
166
+ if (!opts.yes) {
167
+ const proceed = await confirm(`Leave organization '${slug}'?`, false);
168
+ if (!proceed) {
169
+ process.stdout.write("Cancelled.\n");
170
+ return 0;
171
+ }
172
+ }
173
+ await gql(`mutation($id:ID!){ organizationMemberLeave(orgId:$id) }`, { id: org.id });
174
+ ok(`Left organization: ${slug}`);
175
+ return 0;
176
+ }
177
+ catch (err) {
178
+ fail(err.message);
179
+ return 1;
180
+ }
181
+ }
182
+ // ─── member subcommands ──────────────────────────────────────────────
183
+ export async function runOrgMemberList(slug, opts) {
184
+ try {
185
+ const org = await resolveOrg(slug);
186
+ if (!org) {
187
+ fail(`organization '${slug}' not found`);
188
+ return 1;
189
+ }
190
+ const data = await gql(`query($id:ID!){ organizationMembers(orgId:$id){ role joinedAt user{ name email slug } } }`, { id: org.id });
191
+ const members = data.organizationMembers ?? [];
192
+ if (opts.json) {
193
+ emitJson(members);
194
+ return 0;
195
+ }
196
+ process.stdout.write(`Members of ${slug}\n\n`);
197
+ const rows = members.map((m) => ({
198
+ role: m.role,
199
+ name: m.user?.name ?? "",
200
+ email: m.user?.email ?? "",
201
+ namespace: m.user?.slug ?? "",
202
+ joined: String(m.joinedAt ?? "").slice(0, 10),
203
+ }));
204
+ process.stdout.write(rows.length ? renderTable(rows, ["role", "name", "email", "namespace", "joined"]) : " (none)\n");
205
+ process.stdout.write(`\n ${members.length} member(s)\n`);
206
+ return 0;
207
+ }
208
+ catch (err) {
209
+ fail(err.message);
210
+ return 1;
211
+ }
212
+ }
213
+ export async function runOrgMemberAdd(slug, user, opts) {
214
+ try {
215
+ const role = normalizeRole(opts.role);
216
+ const org = await resolveOrg(slug);
217
+ if (!org) {
218
+ fail(`organization '${slug}' not found`);
219
+ return 1;
220
+ }
221
+ const userId = await resolveUserId(user);
222
+ await gql(`mutation($orgId:ID!,$userId:ID!,$role:OrgRole){
223
+ organizationMemberAdd(orgId:$orgId, userId:$userId, role:$role){ id } }`, { orgId: org.id, userId, role: role ?? null });
224
+ ok(`Added ${user} to ${slug} as ${role ?? "MEMBER"}`);
225
+ return 0;
226
+ }
227
+ catch (err) {
228
+ fail(err.message);
229
+ return 1;
230
+ }
231
+ }
232
+ export async function runOrgMemberRole(slug, user, opts) {
233
+ try {
234
+ const role = normalizeRole(opts.role);
235
+ if (!role) {
236
+ fail("--role is required (owner|member)");
237
+ return 1;
238
+ }
239
+ const org = await resolveOrg(slug);
240
+ if (!org) {
241
+ fail(`organization '${slug}' not found`);
242
+ return 1;
243
+ }
244
+ const userId = await resolveUserId(user);
245
+ await gql(`mutation($orgId:ID!,$userId:ID!,$role:OrgRole!){
246
+ organizationMemberUpdateRole(orgId:$orgId, userId:$userId, role:$role){ id } }`, { orgId: org.id, userId, role });
247
+ ok(`Set ${user}'s role in ${slug} to ${role}`);
248
+ return 0;
249
+ }
250
+ catch (err) {
251
+ fail(err.message);
252
+ return 1;
253
+ }
254
+ }
255
+ export async function runOrgMemberRemove(slug, user, opts) {
256
+ try {
257
+ const org = await resolveOrg(slug);
258
+ if (!org) {
259
+ fail(`organization '${slug}' not found`);
260
+ return 1;
261
+ }
262
+ const userId = await resolveUserId(user);
263
+ if (!opts.yes) {
264
+ const proceed = await confirm(`Remove ${user} from ${slug}?`, false);
265
+ if (!proceed) {
266
+ process.stdout.write("Cancelled.\n");
267
+ return 0;
268
+ }
269
+ }
270
+ await gql(`mutation($orgId:ID!,$userId:ID!){ organizationMemberRemove(orgId:$orgId, userId:$userId) }`, { orgId: org.id, userId });
271
+ ok(`Removed ${user} from ${slug}`);
272
+ return 0;
273
+ }
274
+ catch (err) {
275
+ fail(err.message);
276
+ return 1;
277
+ }
278
+ }
279
+ // ─── registration ────────────────────────────────────────────────────
280
+ export function registerOrgCommand(program) {
281
+ const org = program.command("org").description("manage organizations (and members)");
282
+ org
283
+ .command("list")
284
+ .description("list organizations you belong to")
285
+ .option("--json", "emit JSON")
286
+ .action(async (opts) => process.exit(await runOrgList(opts)));
287
+ org
288
+ .command("show")
289
+ .description("show an organization with its members and teams")
290
+ .argument("<slug>", "organization slug")
291
+ .option("--json", "emit JSON")
292
+ .action(async (slug, opts) => process.exit(await runOrgShow(slug, opts)));
293
+ org
294
+ .command("create")
295
+ .description("create an organization (you become OWNER; a namespace is created)")
296
+ .argument("<slug>", "organization slug (3-40 chars, lowercase/hyphens)")
297
+ .option("--name <text>", "display name (default: slug)")
298
+ .option("--description <text>", "description")
299
+ .action(async (slug, opts) => process.exit(await runOrgCreate(slug, opts)));
300
+ org
301
+ .command("update")
302
+ .description("update an organization's name / description")
303
+ .argument("<slug>", "organization slug")
304
+ .option("--name <text>", "new display name")
305
+ .option("--description <text>", "new description")
306
+ .action(async (slug, opts) => process.exit(await runOrgUpdate(slug, opts)));
307
+ org
308
+ .command("delete")
309
+ .description("delete an organization (must have no remaining projects)")
310
+ .argument("<slug>", "organization slug")
311
+ .option("--yes", "skip the confirmation prompt")
312
+ .action(async (slug, opts) => process.exit(await runOrgDelete(slug, opts)));
313
+ org
314
+ .command("leave")
315
+ .description("leave an organization (owners must transfer ownership first)")
316
+ .argument("<slug>", "organization slug")
317
+ .option("--yes", "skip the confirmation prompt")
318
+ .action(async (slug, opts) => process.exit(await runOrgLeave(slug, opts)));
319
+ // ── members ──
320
+ const member = org.command("member").description("manage organization members");
321
+ member
322
+ .command("list")
323
+ .description("list members of an organization")
324
+ .argument("<org>", "organization slug")
325
+ .option("--json", "emit JSON")
326
+ .action(async (slug, opts) => process.exit(await runOrgMemberList(slug, opts)));
327
+ member
328
+ .command("add")
329
+ .description("add a user to an organization (resolve by email or name)")
330
+ .argument("<org>", "organization slug")
331
+ .argument("<user>", "user email or name")
332
+ .option("--role <role>", "owner|member (default: member)")
333
+ .action(async (slug, user, opts) => process.exit(await runOrgMemberAdd(slug, user, opts)));
334
+ member
335
+ .command("role")
336
+ .description("change a member's role")
337
+ .argument("<org>", "organization slug")
338
+ .argument("<user>", "user email or name")
339
+ .requiredOption("--role <role>", "owner|member")
340
+ .action(async (slug, user, opts) => process.exit(await runOrgMemberRole(slug, user, opts)));
341
+ member
342
+ .command("remove")
343
+ .description("remove a member from an organization")
344
+ .argument("<org>", "organization slug")
345
+ .argument("<user>", "user email or name")
346
+ .option("--yes", "skip the confirmation prompt")
347
+ .action(async (slug, user, opts) => process.exit(await runOrgMemberRemove(slug, user, opts)));
348
+ }