faces-cli 1.8.2 → 1.8.3

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.
@@ -10,5 +10,6 @@ export default class TeamAdd extends BaseCommand {
10
10
  static args: {
11
11
  team_id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
12
12
  };
13
+ static examples: string[];
13
14
  run(): Promise<unknown>;
14
15
  }
@@ -4,8 +4,8 @@ import { FacesAPIError } from '../../client.js';
4
4
  import { TeamCatalogService } from '../../team-catalog.js';
5
5
  import { resolveFace } from '../../utils.js';
6
6
  export default class TeamAdd extends BaseCommand {
7
- static description = 'Add face(s) to a team. A team may only contain faces this account owns: a published or shared face ' +
8
- 'can be chatted with but cannot be a member.';
7
+ static description = 'Add face(s) to a team. Any face this account can reach may join: your own, a published one, or one ' +
8
+ 'shared with you. Name someone else\'s as owner:alias, the same address chat uses.';
9
9
  static flags = {
10
10
  ...BaseCommand.baseFlags,
11
11
  face: Flags.string({ description: 'Face alias to add (repeatable)', multiple: true, required: true }),
@@ -13,13 +13,17 @@ export default class TeamAdd extends BaseCommand {
13
13
  static args = {
14
14
  team_id: Args.string({ description: 'Team ID', required: true }),
15
15
  };
16
+ static examples = [
17
+ '<%= config.bin %> <%= command.id %> TEAM_ID --face alice --face bob',
18
+ '<%= config.bin %> <%= command.id %> TEAM_ID --face head:socrates',
19
+ ];
16
20
  async run() {
17
21
  const { args, flags } = await this.parse(TeamAdd);
18
22
  const client = this.makeClient(flags);
19
- // Resolve aliases to iam_ids. resolveFace also finds published and shared
20
- // faces, which the plain face route cannot: without that a reachable face
21
- // fails here as 404 or 403 depending on how it was named, and both read as
22
- // a typo rather than as the rule that teams are owner-scoped.
23
+ // Resolve to iam_ids. The members endpoint takes a uid or an owner:alias
24
+ // and not a bare alias, so resolving here means a user can name any face
25
+ // the way they already name it everywhere else. resolveFace also finds
26
+ // published and shared faces, which the plain face route cannot.
23
27
  const iamIds = [];
24
28
  for (const alias of flags.face) {
25
29
  let resolved;
@@ -36,15 +40,6 @@ export default class TeamAdd extends BaseCommand {
36
40
  }
37
41
  throw err;
38
42
  }
39
- // The server enforces this too, but only once it has an id, which this
40
- // command cannot reach for a face it does not own. Say the real reason.
41
- if (!resolved.owned) {
42
- this.error(`A team may only contain faces you own, and '${resolved.handle}' is owned by ` +
43
- `'${resolved.owner ?? 'another account'}'. It is published or shared with you, so you can chat ` +
44
- `it as ${resolved.handle}@<model>, but it cannot be a team member.\n` +
45
- 'Do not create a copy to work around this: the copy would be a different face with different ' +
46
- 'compiled material.');
47
- }
48
43
  const face = resolved.face;
49
44
  const id = (face.uid ?? face.id ?? face.iam_id);
50
45
  if (!id)
@@ -1,6 +1,7 @@
1
1
  import { BaseCommand } from '../../base.js';
2
2
  export default class TeamMembers extends BaseCommand {
3
3
  static description: string;
4
+ static examples: string[];
4
5
  static flags: {
5
6
  'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
6
7
  token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -1,8 +1,21 @@
1
1
  import { Args } from '@oclif/core';
2
2
  import { BaseCommand } from '../../base.js';
3
3
  import { FacesAPIError } from '../../client.js';
4
+ /**
5
+ * Why a member can no longer be reached, in the terms a user can act on.
6
+ *
7
+ * The two are kept apart deliberately: access can be granted again, a deleted
8
+ * face cannot come back. Rendering them the same would tell someone to go and
9
+ * ask for something that no longer exists.
10
+ */
11
+ const REASONS = {
12
+ unpublished_or_unshared: 'access was revoked by its owner',
13
+ deleted: 'the face was deleted',
14
+ };
4
15
  export default class TeamMembers extends BaseCommand {
5
- static description = 'List members of a team';
16
+ static description = 'List members of a team. A member you do not own can stop being reachable if its owner revokes ' +
17
+ 'access or deletes it; those are listed with the reason rather than dropped.';
18
+ static examples = ['<%= config.bin %> <%= command.id %> TEAM_ID', '<%= config.bin %> <%= command.id %> TEAM_ID --json'];
6
19
  static flags = {
7
20
  ...BaseCommand.baseFlags,
8
21
  };
@@ -17,12 +30,48 @@ export default class TeamMembers extends BaseCommand {
17
30
  data = await client.get(`/v1/teams/${args.team_id}/members`);
18
31
  }
19
32
  catch (err) {
20
- if (err instanceof FacesAPIError)
33
+ if (err instanceof FacesAPIError) {
34
+ if (err.statusCode === 404)
35
+ this.error(`No team '${args.team_id}'. It does not exist, or is not yours.`);
21
36
  this.error(`Error (${err.statusCode}): ${err.message}`);
37
+ }
22
38
  throw err;
23
39
  }
24
- if (!this.jsonEnabled())
25
- this.printHuman(data);
40
+ if (this.jsonEnabled())
41
+ return data;
42
+ const members = (data.data ?? data) ?? [];
43
+ if (members.length === 0) {
44
+ this.log('This team has no members.');
45
+ this.log(`Add one with: faces team:add ${args.team_id} --face <alias>`);
46
+ return data;
47
+ }
48
+ const rows = members.map((m) => ({
49
+ alias: m.alias?.trim() || '(unknown)',
50
+ // reachable is absent on older servers; only an explicit false is a problem.
51
+ status: m.reachable === false
52
+ ? `unreachable - ${REASONS[m.unreachable_reason ?? ''] ?? m.unreachable_reason ?? 'reason not given'}`
53
+ : 'ok',
54
+ id: m.iam_id ?? '-',
55
+ }));
56
+ const cols = [
57
+ ['FACE', (r) => r.alias],
58
+ ['STATUS', (r) => r.status],
59
+ ['ID', (r) => r.id],
60
+ ];
61
+ const w = cols.map(([h, g]) => Math.max(h.length, ...rows.map((r) => g(r).length)));
62
+ const line = (c) => c.map((x, i) => x.padEnd(w[i])).join(' ').trimEnd();
63
+ this.log(line(cols.map(([h]) => h)));
64
+ for (const r of rows)
65
+ this.log(line(cols.map(([, g]) => g(r))));
66
+ const lost = members.filter((m) => m.reachable === false);
67
+ this.log('');
68
+ this.log(`${rows.length} member(s).`);
69
+ if (lost.length > 0) {
70
+ // Say it plainly: a team that silently changed shape is the thing this
71
+ // reporting exists to prevent.
72
+ this.log(`${lost.length} can no longer be reached and will not answer. They are still listed because the ` +
73
+ 'team has not changed; remove them with faces team:remove.');
74
+ }
26
75
  return data;
27
76
  }
28
77
  }