@occam-scaly/scaly-cli 0.2.8 → 0.2.9

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/bin/scaly.js CHANGED
@@ -68,6 +68,15 @@ Usage:
68
68
  scaly auth status [--json]
69
69
  scaly auth logout [--json]
70
70
 
71
+ scaly doctor [--json] [--env-file .env] [--require KEY1,KEY2] [--db-host 127.0.0.1] [--db-port 5432] [--storage-addon <id>] [--storage-prefix <prefix>]
72
+
73
+ scaly user-groups list [--account <accountId>] [--json]
74
+ scaly user-groups members --name <group> [--account <accountId>] [--json]
75
+ scaly user-groups create --name <group> [--description <text>] [--account <accountId>] [--json]
76
+ scaly user-groups delete --name <group> [--yes] [--account <accountId>] [--json]
77
+ scaly user-groups add --name <group> <email...> [--emails a,b] [--account <accountId>] [--json]
78
+ scaly user-groups remove --name <group> <email...> [--emails a,b] [--account <accountId>] [--json]
79
+
71
80
  scaly secrets list --app <appId> [--json]
72
81
  scaly secrets set --app <appId> --name <KEY> [--from-env ENV] [--stdin] [--json]
73
82
  scaly secrets delete --app <appId> (--name <KEY> | --id <secretId>) [--yes] [--json]
@@ -132,6 +141,14 @@ function main(argv) {
132
141
  return runAuth(sub, flags).then((code) => process.exit(code));
133
142
  }
134
143
 
144
+ if (cmd === 'doctor') {
145
+ return runDoctor(args.slice(1)).then((code) => process.exit(code));
146
+ }
147
+
148
+ if (cmd === 'user-groups') {
149
+ return runUserGroups(sub, rest).then((code) => process.exit(code));
150
+ }
151
+
135
152
  if (cmd === 'secrets') {
136
153
  return runSecrets(sub, rest).then((code) => process.exit(code));
137
154
  }
@@ -2610,6 +2627,225 @@ async function runSecrets(sub, rest) {
2610
2627
  return 2;
2611
2628
  }
2612
2629
 
2630
+ // -------------------------
2631
+ // User Groups (account-scoped)
2632
+ // -------------------------
2633
+ async function runUserGroups(sub, rest) {
2634
+ const action = String(sub || '')
2635
+ .trim()
2636
+ .toLowerCase();
2637
+ const f = parseKv(rest);
2638
+ const json = parseBool(f.json, false);
2639
+
2640
+ const accountId = await require('../lib/scaly-user-groups').resolveAccountId(
2641
+ f.account || f['account-id'] || f.account_id
2642
+ );
2643
+ if (!accountId) {
2644
+ const msg =
2645
+ 'Unable to infer account id. Pass --account <accountId> (or ensure you have accessible stacks).';
2646
+ if (json)
2647
+ console.log(JSON.stringify({ ok: false, error: { message: msg } }));
2648
+ else console.error(msg);
2649
+ return 2;
2650
+ }
2651
+
2652
+ const groups = require('../lib/scaly-user-groups');
2653
+
2654
+ try {
2655
+ if (!action || action === 'list') {
2656
+ const res = await groups.listGroups({ accountId });
2657
+ const out = {
2658
+ ok: true,
2659
+ account_id: accountId,
2660
+ user_groups: { addon_id: res.pool.id, name: res.pool.name },
2661
+ groups: (res.groups || []).map((g) => ({
2662
+ name: g.name,
2663
+ description: g.description || null
2664
+ }))
2665
+ };
2666
+ if (json) console.log(JSON.stringify(out));
2667
+ else out.groups.forEach((g) => console.log(g.name));
2668
+ return 0;
2669
+ }
2670
+
2671
+ if (action === 'members') {
2672
+ const name = f.name || (f._ && f._[0]);
2673
+ if (!name) {
2674
+ const msg = '--name <group> is required';
2675
+ if (json)
2676
+ console.log(JSON.stringify({ ok: false, error: { message: msg } }));
2677
+ else console.error(msg);
2678
+ return 2;
2679
+ }
2680
+ const res = await groups.listGroupMembers({ accountId, name });
2681
+ const out = {
2682
+ ok: true,
2683
+ account_id: accountId,
2684
+ group: { name, description: res.group.description || null },
2685
+ members: (res.members || []).map((u) => ({
2686
+ email: u.email,
2687
+ name: u.name,
2688
+ status: u.status || null,
2689
+ enabled: typeof u.enabled === 'boolean' ? u.enabled : null
2690
+ }))
2691
+ };
2692
+ if (json) console.log(JSON.stringify(out));
2693
+ else out.members.forEach((u) => console.log(u.email));
2694
+ return 0;
2695
+ }
2696
+
2697
+ if (action === 'create') {
2698
+ const name = f.name || (f._ && f._[0]);
2699
+ if (!name) {
2700
+ const msg = '--name <group> is required';
2701
+ if (json)
2702
+ console.log(JSON.stringify({ ok: false, error: { message: msg } }));
2703
+ else console.error(msg);
2704
+ return 2;
2705
+ }
2706
+ const res = await groups.createGroup({
2707
+ accountId,
2708
+ name,
2709
+ description: f.description || null
2710
+ });
2711
+ const out = {
2712
+ ok: true,
2713
+ account_id: accountId,
2714
+ user_groups: { addon_id: res.pool.id, name: res.pool.name },
2715
+ created: res.created
2716
+ };
2717
+ if (json) console.log(JSON.stringify(out));
2718
+ else console.log(`Created group: ${name}`);
2719
+ return 0;
2720
+ }
2721
+
2722
+ if (action === 'delete') {
2723
+ const name = f.name || (f._ && f._[0]);
2724
+ const yes = parseBool(f.yes, false);
2725
+ if (!name) {
2726
+ const msg = '--name <group> is required';
2727
+ if (json)
2728
+ console.log(JSON.stringify({ ok: false, error: { message: msg } }));
2729
+ else console.error(msg);
2730
+ return 2;
2731
+ }
2732
+ if (!yes) {
2733
+ const msg = 'Refusing to delete group without --yes';
2734
+ if (json)
2735
+ console.log(JSON.stringify({ ok: false, error: { message: msg } }));
2736
+ else console.error(msg);
2737
+ return 2;
2738
+ }
2739
+ const res = await groups.deleteGroup({ accountId, name });
2740
+ const out = {
2741
+ ok: true,
2742
+ account_id: accountId,
2743
+ deleted: true,
2744
+ count: res.result ? res.result.count : null
2745
+ };
2746
+ if (json) console.log(JSON.stringify(out));
2747
+ else console.log(`Deleted group: ${name}`);
2748
+ return 0;
2749
+ }
2750
+
2751
+ if (action === 'add' || action === 'remove') {
2752
+ const name = f.name || (f._ && f._[0]);
2753
+ if (!name) {
2754
+ const msg = '--name <group> is required';
2755
+ if (json)
2756
+ console.log(JSON.stringify({ ok: false, error: { message: msg } }));
2757
+ else console.error(msg);
2758
+ return 2;
2759
+ }
2760
+ const rawEmails = []
2761
+ .concat(f.emails ? String(f.emails).split(',') : [])
2762
+ .concat(f.email ? [String(f.email)] : [])
2763
+ .concat(Array.isArray(f._) ? f._.slice(1) : []);
2764
+ const emails = rawEmails.map((e) => String(e).trim()).filter(Boolean);
2765
+ if (!emails.length) {
2766
+ const msg =
2767
+ 'At least one email is required (positional args or --emails a,b)';
2768
+ if (json)
2769
+ console.log(JSON.stringify({ ok: false, error: { message: msg } }));
2770
+ else console.error(msg);
2771
+ return 2;
2772
+ }
2773
+ const res =
2774
+ action === 'add'
2775
+ ? await groups.addUsers({ accountId, name, emails })
2776
+ : await groups.removeUsers({ accountId, name, emails });
2777
+ const out = {
2778
+ ok: true,
2779
+ account_id: accountId,
2780
+ group: name,
2781
+ emails,
2782
+ count: res.result ? res.result.count : null
2783
+ };
2784
+ if (json) console.log(JSON.stringify(out));
2785
+ else
2786
+ console.log(
2787
+ `${action === 'add' ? 'Added' : 'Removed'} ${emails.length} user(s).`
2788
+ );
2789
+ return 0;
2790
+ }
2791
+
2792
+ const msg = `Unknown user-groups command: ${action}`;
2793
+ if (json)
2794
+ console.log(JSON.stringify({ ok: false, error: { message: msg } }));
2795
+ else console.error(msg);
2796
+ return 2;
2797
+ } catch (e) {
2798
+ const msg = String((e && e.message) || e);
2799
+ if (json)
2800
+ console.log(
2801
+ JSON.stringify({
2802
+ ok: false,
2803
+ error: { message: msg, code: e && e.code ? e.code : null }
2804
+ })
2805
+ );
2806
+ else console.error(msg);
2807
+ return 2;
2808
+ }
2809
+ }
2810
+
2811
+ // -------------------------
2812
+ // Doctor (local checklist)
2813
+ // -------------------------
2814
+ async function runDoctor(rest) {
2815
+ const f = parseKv(rest);
2816
+ const json = parseBool(f.json, false);
2817
+ const required =
2818
+ f.require && typeof f.require === 'string'
2819
+ ? f.require
2820
+ .split(',')
2821
+ .map((s) => s.trim())
2822
+ .filter(Boolean)
2823
+ : [];
2824
+
2825
+ const { runDoctor: doctor } = require('../lib/scaly-doctor');
2826
+ const res = await doctor({
2827
+ envFile: f['env-file'] || f.env_file || '.env',
2828
+ requiredEnvVars: required,
2829
+ dbHost: f['db-host'] || f.db_host || '127.0.0.1',
2830
+ dbPort:
2831
+ f['db-port'] !== undefined ? Number(f['db-port'] || f.db_port) : null,
2832
+ storageAddonId: f['storage-addon'] || f.storage_addon || null,
2833
+ storagePrefix: f['storage-prefix'] || f.storage_prefix || null
2834
+ });
2835
+
2836
+ if (json) {
2837
+ console.log(JSON.stringify(res));
2838
+ } else {
2839
+ for (const c of res.checks) {
2840
+ const status = c.ok ? 'OK' : 'FAIL';
2841
+ console.log(`[doctor] ${status} ${c.name}`);
2842
+ if (c.hint) console.log(` ${c.hint}`);
2843
+ }
2844
+ }
2845
+
2846
+ return res.ok ? 0 : 1;
2847
+ }
2848
+
2613
2849
  // -------------------------
2614
2850
  // DB tunnel: localhost port-forward via WSS
2615
2851
  // -------------------------
package/lib/scaly-api.js CHANGED
@@ -307,6 +307,26 @@ const QUERIES = {
307
307
  minIdle
308
308
  }
309
309
  }
310
+ `,
311
+ listGroups: `
312
+ query ListGroups($where: AddOnWhereUniqueInput!) {
313
+ listGroups(where: $where) { name description }
314
+ }
315
+ `,
316
+ listGroupsWithUsers: `
317
+ query ListGroupsWithUsers($where: AddOnWhereUniqueInput!) {
318
+ listGroups(where: $where) { name description users { email name status enabled } }
319
+ }
320
+ `,
321
+ listBucketObjects: `
322
+ query ListBucketObjects($where: AddOnWhereUniqueInput!, $prefix: String, $pageSize: Int) {
323
+ listBucketObjects(where: $where, prefix: $prefix, pageSize: $pageSize) {
324
+ bucket
325
+ prefix
326
+ nextCursor
327
+ objects { key size lastModified }
328
+ }
329
+ }
310
330
  `
311
331
  };
312
332
 
@@ -335,6 +355,26 @@ const MUTATIONS = {
335
355
  mutation CreateAddOn($data: AddOnCreateInput!) {
336
356
  createAddOn(data: $data) { id name type accountId status }
337
357
  }
358
+ `,
359
+ createUserGroups: `
360
+ mutation CreateUserGroups($where: AddOnWhereUniqueInput!, $data: [CognitoUserGroupInput!]!) {
361
+ createUserGroups(where: $where, data: $data) { name description }
362
+ }
363
+ `,
364
+ deleteUserGroups: `
365
+ mutation DeleteUserGroups($where: AddOnWhereUniqueInput!, $groups: [String!]!) {
366
+ deleteUserGroups(where: $where, groups: $groups) { count }
367
+ }
368
+ `,
369
+ addUsersToGroups: `
370
+ mutation AddUsersToGroups($where: AddOnWhereUniqueInput!, $emails: [String!]!, $groups: [String!]!) {
371
+ addUsersToGroups(where: $where, emails: $emails, groups: $groups) { count }
372
+ }
373
+ `,
374
+ removeUsersFromGroups: `
375
+ mutation RemoveUsersFromGroups($where: AddOnWhereUniqueInput!, $emails: [String!]!, $groups: [String!]!) {
376
+ removeUsersFromGroups(where: $where, emails: $emails, groups: $groups) { count }
377
+ }
338
378
  `
339
379
  };
340
380
 
@@ -384,6 +424,57 @@ async function listAddOnsByType({ accountId, type, take = 10 }) {
384
424
  return data?.listAddOns || [];
385
425
  }
386
426
 
427
+ async function listGroups({ addonId, includeUsers = false }) {
428
+ const data = await graphqlRequest(
429
+ includeUsers ? QUERIES.listGroupsWithUsers : QUERIES.listGroups,
430
+ { where: { id: addonId } }
431
+ );
432
+ return data?.listGroups || [];
433
+ }
434
+
435
+ async function createUserGroup({ addonId, name, description }) {
436
+ const data = await graphqlRequest(MUTATIONS.createUserGroups, {
437
+ where: { id: addonId },
438
+ data: [{ name, description: description || null }]
439
+ });
440
+ return (data?.createUserGroups || [])[0] || null;
441
+ }
442
+
443
+ async function deleteUserGroup({ addonId, name }) {
444
+ const data = await graphqlRequest(MUTATIONS.deleteUserGroups, {
445
+ where: { id: addonId },
446
+ groups: [name]
447
+ });
448
+ return data?.deleteUserGroups || null;
449
+ }
450
+
451
+ async function addUsersToGroup({ addonId, name, emails }) {
452
+ const data = await graphqlRequest(MUTATIONS.addUsersToGroups, {
453
+ where: { id: addonId },
454
+ emails,
455
+ groups: [name]
456
+ });
457
+ return data?.addUsersToGroups || null;
458
+ }
459
+
460
+ async function removeUsersFromGroup({ addonId, name, emails }) {
461
+ const data = await graphqlRequest(MUTATIONS.removeUsersFromGroups, {
462
+ where: { id: addonId },
463
+ emails,
464
+ groups: [name]
465
+ });
466
+ return data?.removeUsersFromGroups || null;
467
+ }
468
+
469
+ async function listBucketObjects({ addonId, prefix, pageSize = 1 }) {
470
+ const data = await graphqlRequest(QUERIES.listBucketObjects, {
471
+ where: { id: addonId },
472
+ prefix: prefix || null,
473
+ pageSize
474
+ });
475
+ return data?.listBucketObjects || null;
476
+ }
477
+
387
478
  async function createStack({
388
479
  accountId,
389
480
  name,
@@ -503,6 +594,12 @@ module.exports = {
503
594
  listAddOnsByType,
504
595
  listStacks,
505
596
  listApps,
597
+ listGroups,
598
+ createUserGroup,
599
+ deleteUserGroup,
600
+ addUsersToGroup,
601
+ removeUsersFromGroup,
602
+ listBucketObjects,
506
603
  createStack,
507
604
  updateStack,
508
605
  createApp,
@@ -0,0 +1,211 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const net = require('net');
5
+ const path = require('path');
6
+
7
+ const api = require('./scaly-api');
8
+ const { normalizeStage, readAuthStore } = require('./scaly-auth');
9
+
10
+ function parseEnvKeys(text) {
11
+ const out = new Set();
12
+ for (const rawLine of String(text || '').split('\n')) {
13
+ const line = rawLine.trim();
14
+ if (!line || line.startsWith('#')) continue;
15
+ const idx = line.indexOf('=');
16
+ if (idx <= 0) continue;
17
+ out.add(line.slice(0, idx).trim());
18
+ }
19
+ return out;
20
+ }
21
+
22
+ function tcpCheck(host, port, timeoutMs = 1000) {
23
+ return new Promise((resolve) => {
24
+ const socket = net.createConnection({ host, port });
25
+ const done = (ok) => {
26
+ try {
27
+ socket.destroy();
28
+ } catch {}
29
+ resolve(ok);
30
+ };
31
+ socket.setTimeout(timeoutMs);
32
+ socket.once('connect', () => done(true));
33
+ socket.once('timeout', () => done(false));
34
+ socket.once('error', () => done(false));
35
+ });
36
+ }
37
+
38
+ async function runDoctor({
39
+ envFile = '.env',
40
+ requiredEnvVars = [],
41
+ dbHost = '127.0.0.1',
42
+ dbPort = null,
43
+ storageAddonId = null,
44
+ storagePrefix = null
45
+ } = {}) {
46
+ const checks = [];
47
+
48
+ // Auth store / token
49
+ const found = readAuthStore();
50
+ const session = found && found.session ? found.session : null;
51
+ const stage = normalizeStage(
52
+ (session && session.stage) || process.env.SCALY_STAGE || 'prod'
53
+ );
54
+ const expMs =
55
+ session && typeof session.expires_at === 'number'
56
+ ? session.expires_at
57
+ : null;
58
+ const authenticated = Boolean(session && session.access_token);
59
+ const expired = Boolean(expMs && expMs <= Date.now());
60
+
61
+ checks.push({
62
+ name: 'auth',
63
+ ok: authenticated && !expired,
64
+ details: {
65
+ authenticated,
66
+ stage: session ? session.stage || stage : stage,
67
+ expires_in_seconds: expMs
68
+ ? Math.max(0, Math.floor((expMs - Date.now()) / 1000))
69
+ : null,
70
+ store_path: found ? found.path : null
71
+ },
72
+ hint: !authenticated
73
+ ? 'Run `scaly auth login`.'
74
+ : expired
75
+ ? `Run \`scaly auth login --stage ${stage}\`.`
76
+ : null
77
+ });
78
+
79
+ // API reachability
80
+ if (!process.env.SCALY_API_KEY) {
81
+ checks.push({
82
+ name: 'api',
83
+ ok: false,
84
+ details: { skipped: true },
85
+ hint: 'Set SCALY_API_KEY to verify API reachability.'
86
+ });
87
+ } else {
88
+ try {
89
+ const startedAt = Date.now();
90
+ await api.listStacks({ take: 1 });
91
+ checks.push({
92
+ name: 'api',
93
+ ok: true,
94
+ details: {
95
+ endpoint: api.resolveApiEndpoint(),
96
+ latency_ms: Date.now() - startedAt
97
+ }
98
+ });
99
+ } catch (e) {
100
+ checks.push({
101
+ name: 'api',
102
+ ok: false,
103
+ details: {
104
+ endpoint: api.resolveApiEndpoint(),
105
+ error: String((e && e.message) || e)
106
+ },
107
+ hint: 'Verify SCALY_API_KEY / SCALY_STAGE and confirm the API is reachable.'
108
+ });
109
+ }
110
+ }
111
+
112
+ // .env key checklist (optional)
113
+ if (Array.isArray(requiredEnvVars) && requiredEnvVars.length > 0) {
114
+ try {
115
+ const abs = path.resolve(envFile);
116
+ if (!fs.existsSync(abs)) {
117
+ checks.push({
118
+ name: 'env',
119
+ ok: false,
120
+ details: {
121
+ env_file: envFile,
122
+ missing_file: true,
123
+ required: requiredEnvVars
124
+ },
125
+ hint: `Create ${envFile} (or pass --env-file <path>).`
126
+ });
127
+ } else {
128
+ const keys = parseEnvKeys(fs.readFileSync(abs, 'utf8'));
129
+ const missing = requiredEnvVars.filter((k) => !keys.has(k));
130
+ checks.push({
131
+ name: 'env',
132
+ ok: missing.length === 0,
133
+ details: { env_file: envFile, required: requiredEnvVars, missing },
134
+ hint: missing.length
135
+ ? `Add missing keys to ${envFile} (values are not shown).`
136
+ : null
137
+ });
138
+ }
139
+ } catch (e) {
140
+ checks.push({
141
+ name: 'env',
142
+ ok: false,
143
+ details: { env_file: envFile, error: String((e && e.message) || e) }
144
+ });
145
+ }
146
+ }
147
+
148
+ // DB tunnel port check (optional)
149
+ if (typeof dbPort === 'number') {
150
+ const okTcp = await tcpCheck(dbHost, dbPort, 1000);
151
+ checks.push({
152
+ name: 'db_tunnel',
153
+ ok: okTcp,
154
+ details: { host: dbHost, port: dbPort },
155
+ hint: okTcp ? null : 'Start a DB tunnel (CLI or MCP) and retry.'
156
+ });
157
+ }
158
+
159
+ // Storage access check (optional)
160
+ if (storageAddonId) {
161
+ if (!process.env.SCALY_API_KEY) {
162
+ checks.push({
163
+ name: 'storage',
164
+ ok: false,
165
+ details: { addon_id: storageAddonId, skipped: true },
166
+ hint: 'Set SCALY_API_KEY and run `scaly auth login` to verify storage access.'
167
+ });
168
+ } else if (!authenticated || expired) {
169
+ checks.push({
170
+ name: 'storage',
171
+ ok: false,
172
+ details: { addon_id: storageAddonId, skipped: true },
173
+ hint: `Run \`scaly auth login --stage ${stage}\` to enable storage checks.`
174
+ });
175
+ } else {
176
+ try {
177
+ const startedAt = Date.now();
178
+ const res = await api.listBucketObjects({
179
+ addonId: storageAddonId,
180
+ prefix: storagePrefix,
181
+ pageSize: 1
182
+ });
183
+ checks.push({
184
+ name: 'storage',
185
+ ok: true,
186
+ details: {
187
+ addon_id: storageAddonId,
188
+ bucket: res ? res.bucket : null,
189
+ prefix: storagePrefix,
190
+ latency_ms: Date.now() - startedAt
191
+ }
192
+ });
193
+ } catch (e) {
194
+ checks.push({
195
+ name: 'storage',
196
+ ok: false,
197
+ details: {
198
+ addon_id: storageAddonId,
199
+ error: String((e && e.message) || e)
200
+ },
201
+ hint: 'Verify storage add-on id and that your session token is valid.'
202
+ });
203
+ }
204
+ }
205
+ }
206
+
207
+ const ok = checks.every((c) => c.ok !== false);
208
+ return { ok, checks };
209
+ }
210
+
211
+ module.exports = { runDoctor };
@@ -0,0 +1,97 @@
1
+ 'use strict';
2
+
3
+ const api = require('./scaly-api');
4
+
5
+ async function resolveAccountId(explicit) {
6
+ if (explicit) return explicit;
7
+ const stacks = await api.listStacks({ take: 1 });
8
+ return stacks && stacks[0] ? stacks[0].accountId : null;
9
+ }
10
+
11
+ async function getAccountUserGroupsAddOn(accountId) {
12
+ const pools = await api.listAddOnsByType({
13
+ accountId,
14
+ type: 'COGNITO',
15
+ take: 5
16
+ });
17
+ if (!pools || pools.length === 0) {
18
+ const e = new Error(
19
+ 'User Groups is not provisioned for this account. Contact support to repair this account.'
20
+ );
21
+ e.code = 'COGNITO_POOL_MISSING';
22
+ throw e;
23
+ }
24
+ if (pools.length > 1) {
25
+ const e = new Error(
26
+ 'Multiple User Groups pools exist in this account. Contact support to repair this account before continuing.'
27
+ );
28
+ e.code = 'COGNITO_POOL_DUPLICATE';
29
+ e.details = { pools };
30
+ throw e;
31
+ }
32
+ return pools[0];
33
+ }
34
+
35
+ async function listGroups({ accountId }) {
36
+ const pool = await getAccountUserGroupsAddOn(accountId);
37
+ const groups = await api.listGroups({
38
+ addonId: pool.id,
39
+ includeUsers: false
40
+ });
41
+ return { pool, groups };
42
+ }
43
+
44
+ async function listGroupMembers({ accountId, name }) {
45
+ const pool = await getAccountUserGroupsAddOn(accountId);
46
+ const groups = await api.listGroups({ addonId: pool.id, includeUsers: true });
47
+ const g = (groups || []).find((x) => x && x.name === name) || null;
48
+ if (!g) {
49
+ const e = new Error(`Group not found: ${name}`);
50
+ e.code = 'GROUP_NOT_FOUND';
51
+ throw e;
52
+ }
53
+ return { pool, group: g, members: g.users || [] };
54
+ }
55
+
56
+ async function createGroup({ accountId, name, description }) {
57
+ const pool = await getAccountUserGroupsAddOn(accountId);
58
+ const created = await api.createUserGroup({
59
+ addonId: pool.id,
60
+ name,
61
+ description
62
+ });
63
+ return { pool, created };
64
+ }
65
+
66
+ async function deleteGroup({ accountId, name }) {
67
+ const pool = await getAccountUserGroupsAddOn(accountId);
68
+ const res = await api.deleteUserGroup({ addonId: pool.id, name });
69
+ return { pool, result: res };
70
+ }
71
+
72
+ async function addUsers({ accountId, name, emails }) {
73
+ const pool = await getAccountUserGroupsAddOn(accountId);
74
+ const res = await api.addUsersToGroup({ addonId: pool.id, name, emails });
75
+ return { pool, result: res };
76
+ }
77
+
78
+ async function removeUsers({ accountId, name, emails }) {
79
+ const pool = await getAccountUserGroupsAddOn(accountId);
80
+ const res = await api.removeUsersFromGroup({
81
+ addonId: pool.id,
82
+ name,
83
+ emails
84
+ });
85
+ return { pool, result: res };
86
+ }
87
+
88
+ module.exports = {
89
+ resolveAccountId,
90
+ getAccountUserGroupsAddOn,
91
+ listGroups,
92
+ listGroupMembers,
93
+ createGroup,
94
+ deleteGroup,
95
+ addUsers,
96
+ removeUsers
97
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@occam-scaly/scaly-cli",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Scaly CLI (auth + project config helpers)",
5
5
  "bin": {
6
6
  "scaly": "./bin/scaly.js"