@homespunapps/cli 1.0.0 → 1.4.2

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.
@@ -1,48 +1,7 @@
1
1
  import { assertKnownFlags } from "../argv.js";
2
+ import { specFor } from "../help-catalog.js";
2
3
  import { makeClient } from "../config.js";
3
4
  import { printJson, fail, failFromError } from "../output.js";
4
- const CREATE_FLAGS = ["type", "message", "app-id"];
5
- const LIST_FLAGS = ["limit", "before"];
6
- const NO_BOOLS = [];
7
- export const feedbackHelp = `homespun feedback — submit / list feedback to the relay operator
8
-
9
- Feedback is a one-shot bug report, feature request, or note from YOUR agent
10
- to whoever runs the relay. Submissions are stored in the relay DB; the
11
- operator triages out of band.
12
-
13
- Usage:
14
- homespun feedback <subcommand> [options]
15
-
16
- Subcommands:
17
- create Submit one feedback row. Requires --type and --message.
18
- Prints { id, type, created_at } — the message is not echoed back.
19
-
20
- list List YOUR agent's own submissions, newest first. Prints
21
- { items: [...], next_before?: <cursor> }. Pass --before <cursor>
22
- from a previous page to fetch the next page.
23
-
24
- Options for 'create':
25
- --type <bug|feature|note> Feedback category. Required.
26
- --message <text|-> Message body. Pass '-' to read from stdin.
27
- 1..4000 chars after trim.
28
- --app-id <id> Optional App this feedback relates to;
29
- must be owned by YOUR agent's human.
30
-
31
- Options for 'list':
32
- --limit <N> Page size (default 50, max 100).
33
- --before <cursor> Opaque cursor from a previous page's next_before.
34
-
35
- Global:
36
- --url <url> Relay base URL (overrides HOMESPUN_URL).
37
- --api-key <key> Agent API key (overrides HOMESPUN_API_KEY).
38
- -h, --help Show this help.
39
-
40
- Examples:
41
- homespun feedback create --type bug --message "watch hangs on empty app"
42
- echo "long-form note..." | homespun feedback create --type note --message -
43
- homespun feedback list --limit 20
44
-
45
- Output: stdout is machine-readable JSON.`;
46
5
  const FEEDBACK_TYPES = ["bug", "feature", "note"];
47
6
  async function readStdin() {
48
7
  const chunks = [];
@@ -52,7 +11,7 @@ async function readStdin() {
52
11
  return Buffer.concat(chunks).toString("utf8");
53
12
  }
54
13
  async function runFeedbackCreate(args) {
55
- assertKnownFlags(args, CREATE_FLAGS, NO_BOOLS, "homespun feedback create");
14
+ assertKnownFlags(args, ...specFor("feedback", "create"));
56
15
  const type = args.flags.get("type");
57
16
  const rawMessage = args.flags.get("message");
58
17
  const appId = args.flags.get("app-id");
@@ -92,7 +51,7 @@ async function runFeedbackCreate(args) {
92
51
  }
93
52
  }
94
53
  async function runFeedbackList(args) {
95
- assertKnownFlags(args, LIST_FLAGS, NO_BOOLS, "homespun feedback list");
54
+ assertKnownFlags(args, ...specFor("feedback", "list"));
96
55
  const limitRaw = args.flags.get("limit");
97
56
  const before = args.flags.get("before");
98
57
  let limit;
@@ -0,0 +1,158 @@
1
+ // `homespun grants` (M5) grant-link management for a v2 app: mint a
2
+ // capability URL that confers a DECLARED custom role on a stable, per-holder
3
+ // anonymous identity, list an app's grant links, and revoke one. Every verb
4
+ // targets an app via a required `--app <idOrSlug>` flag, resolved the same way
5
+ // `homespun members`/`homespun data` do (resolveAppId).
6
+ //
7
+ // Auth on the relay side is owner-or-agent; this CLI always authenticates as
8
+ // the owning agent, so any verb works for an app the calling agent's owning
9
+ // human owns.
10
+ //
11
+ // A grant link NEVER escalates: --role must be a DECLARED custom role
12
+ // (x-homespun-manifest.roles), never a built-in role (owner/member/agent). The
13
+ // minted grant_url carries the raw token in its #g= fragment and is printed
14
+ // ONCE (it is never recoverable afterward).
15
+ import { assertKnownFlags } from "../argv.js";
16
+ import { nounSpec, renderNounHelp, specFor } from "../help-catalog.js";
17
+ import { makeClient } from "../config.js";
18
+ import { fail, failFromError, printJson } from "../output.js";
19
+ import { resolveAppId } from "../resolve-app.js";
20
+ export async function runGrant(args) {
21
+ const verb = args.positionals[0];
22
+ if ((verb === undefined || verb === "help") && args.bools.has("help")) {
23
+ process.stdout.write(renderNounHelp(nounSpec("grants")) + "\n");
24
+ return;
25
+ }
26
+ if (verb === undefined) {
27
+ fail("missing verb: homespun grants <mint|list|revoke>", "invalid_args");
28
+ }
29
+ const sub = {
30
+ positionals: args.positionals.slice(1),
31
+ flags: args.flags,
32
+ bools: args.bools,
33
+ ...(args.danglingValueFlags !== undefined
34
+ ? { danglingValueFlags: args.danglingValueFlags }
35
+ : {}),
36
+ };
37
+ switch (verb) {
38
+ case "mint":
39
+ return runMint(sub);
40
+ case "list":
41
+ return runList(sub);
42
+ case "revoke":
43
+ return runRevoke(sub);
44
+ default:
45
+ fail(`unknown verb '${verb}' (homespun grants <mint|list|revoke>)`, "invalid_args");
46
+ }
47
+ }
48
+ function parsePositiveInt(raw, flag) {
49
+ const n = Number(raw);
50
+ if (!Number.isInteger(n) || n <= 0) {
51
+ fail(`${flag} must be a positive integer`, "invalid_args");
52
+ }
53
+ return n;
54
+ }
55
+ // ---------------------------------------------------------------------------
56
+ // mint
57
+ // ---------------------------------------------------------------------------
58
+ async function runMint(args) {
59
+ assertKnownFlags(args, ...specFor("grants", "mint"));
60
+ const appArg = args.flags.get("app");
61
+ if (!appArg) {
62
+ fail("usage: homespun grants mint --app <idOrSlug> --role <customRole>", "invalid_args");
63
+ }
64
+ const role = args.flags.get("role");
65
+ if (!role) {
66
+ fail("--role is required", "invalid_args");
67
+ }
68
+ const mode = args.flags.get("mode");
69
+ if (mode !== undefined && mode !== "once" && mode !== "multi") {
70
+ fail('--mode must be "once" or "multi"', "invalid_args");
71
+ }
72
+ const pinRow = args.flags.get("pin-row");
73
+ const pinWhereRaw = args.flags.get("pin-where");
74
+ if (pinRow !== undefined && pinWhereRaw !== undefined) {
75
+ fail("--pin-row and --pin-where are mutually exclusive", "invalid_args");
76
+ }
77
+ let pin;
78
+ if (pinRow !== undefined) {
79
+ pin = { rowKey: pinRow };
80
+ }
81
+ else if (pinWhereRaw !== undefined) {
82
+ let parsed;
83
+ try {
84
+ parsed = JSON.parse(pinWhereRaw);
85
+ }
86
+ catch {
87
+ fail("--pin-where must be a JSON array of conditions", "invalid_args");
88
+ }
89
+ if (!Array.isArray(parsed)) {
90
+ fail("--pin-where must be a JSON array of conditions", "invalid_args");
91
+ }
92
+ pin = { where: parsed };
93
+ }
94
+ const maxUsesRaw = args.flags.get("max-uses");
95
+ const ttlRaw = args.flags.get("ttl");
96
+ const client = makeClient(args);
97
+ const appId = await resolveAppId(client, appArg);
98
+ try {
99
+ printJson(await client.mintAppGrant(appId, {
100
+ role: role,
101
+ ...(mode !== undefined ? { mode: mode } : {}),
102
+ ...(maxUsesRaw !== undefined
103
+ ? { maxUses: parsePositiveInt(maxUsesRaw, "--max-uses") }
104
+ : {}),
105
+ ...(args.flags.get("label") !== undefined
106
+ ? { label: args.flags.get("label") }
107
+ : {}),
108
+ ...(ttlRaw !== undefined
109
+ ? { ttlSeconds: parsePositiveInt(ttlRaw, "--ttl") }
110
+ : {}),
111
+ ...(pin !== undefined ? { pin } : {}),
112
+ }));
113
+ }
114
+ catch (e) {
115
+ failFromError(e);
116
+ }
117
+ }
118
+ // ---------------------------------------------------------------------------
119
+ // list
120
+ // ---------------------------------------------------------------------------
121
+ async function runList(args) {
122
+ assertKnownFlags(args, ...specFor("grants", "list"));
123
+ const appArg = args.flags.get("app");
124
+ if (!appArg) {
125
+ fail("usage: homespun grants list --app <idOrSlug>", "invalid_args");
126
+ }
127
+ const client = makeClient(args);
128
+ const appId = await resolveAppId(client, appArg);
129
+ try {
130
+ printJson(await client.listAppGrants(appId));
131
+ }
132
+ catch (e) {
133
+ failFromError(e);
134
+ }
135
+ }
136
+ // ---------------------------------------------------------------------------
137
+ // revoke
138
+ // ---------------------------------------------------------------------------
139
+ async function runRevoke(args) {
140
+ assertKnownFlags(args, ...specFor("grants", "revoke"));
141
+ const appArg = args.flags.get("app");
142
+ if (!appArg) {
143
+ fail("usage: homespun grants revoke --app <idOrSlug> --grant <grantId>", "invalid_args");
144
+ }
145
+ const grantId = args.flags.get("grant");
146
+ if (!grantId) {
147
+ fail("--grant is required", "invalid_args");
148
+ }
149
+ const client = makeClient(args);
150
+ const appId = await resolveAppId(client, appArg);
151
+ try {
152
+ await client.revokeAppGrant(appId, grantId);
153
+ printJson({ revoked: true, app_id: appId, grant_id: grantId });
154
+ }
155
+ catch (e) {
156
+ failFromError(e);
157
+ }
158
+ }
@@ -0,0 +1,85 @@
1
+ // `homespun ingest`: inbound catch-hook read surface for an app (inbound-webhooks
2
+ // PR 3). List the app's declared hooks with their full secret URL, and rotate a
3
+ // hook's secret. Every verb targets an app via a required `--app <idOrSlug>`,
4
+ // resolved the same way `homespun members`/`homespun data` do (resolveAppId).
5
+ //
6
+ // This is the smallest surface an agent needs during app setup: after deploying
7
+ // a manifest that declares an `ingest` hook, the agent runs `homespun ingest list`
8
+ // to read back the exact URL and tells its owner to paste it into Stripe/Zapier/
9
+ // Home Assistant/etc. Hooks are manifest-declared, so there is no create/delete
10
+ // here; `rotate` re-keys a leaked URL without a redeploy.
11
+ //
12
+ // Auth on the relay side is owner-or-agent (the owning agent's API key OR the
13
+ // owner human's login cookie); this CLI always authenticates as the agent, so
14
+ // both verbs work for an app the calling agent's owning human owns.
15
+ import { assertKnownFlags } from "../argv.js";
16
+ import { nounSpec, renderNounHelp, specFor } from "../help-catalog.js";
17
+ import { makeClient } from "../config.js";
18
+ import { fail, failFromError, printJson } from "../output.js";
19
+ import { resolveAppId } from "../resolve-app.js";
20
+ export async function runIngest(args) {
21
+ const verb = args.positionals[0];
22
+ if ((verb === undefined || verb === "help") && args.bools.has("help")) {
23
+ process.stdout.write(renderNounHelp(nounSpec("ingest")) + "\n");
24
+ return;
25
+ }
26
+ if (verb === undefined) {
27
+ fail("missing verb (homespun ingest <list|rotate>)", "invalid_args");
28
+ }
29
+ const sub = {
30
+ positionals: args.positionals.slice(1),
31
+ flags: args.flags,
32
+ bools: args.bools,
33
+ ...(args.danglingValueFlags !== undefined
34
+ ? { danglingValueFlags: args.danglingValueFlags }
35
+ : {}),
36
+ };
37
+ switch (verb) {
38
+ case "list":
39
+ return runList(sub);
40
+ case "rotate":
41
+ return runRotate(sub);
42
+ default:
43
+ fail(`unknown verb '${verb}' (homespun ingest <list|rotate>)`, "invalid_args");
44
+ }
45
+ }
46
+ // ---------------------------------------------------------------------------
47
+ // list
48
+ // ---------------------------------------------------------------------------
49
+ async function runList(args) {
50
+ assertKnownFlags(args, ...specFor("ingest", "list"));
51
+ const appArg = args.flags.get("app");
52
+ if (!appArg) {
53
+ fail("usage: homespun ingest list --app <idOrSlug>", "invalid_args");
54
+ }
55
+ const client = makeClient(args);
56
+ const appId = await resolveAppId(client, appArg);
57
+ try {
58
+ printJson(await client.listIngestHooks(appId));
59
+ }
60
+ catch (e) {
61
+ failFromError(e);
62
+ }
63
+ }
64
+ // ---------------------------------------------------------------------------
65
+ // rotate
66
+ // ---------------------------------------------------------------------------
67
+ async function runRotate(args) {
68
+ assertKnownFlags(args, ...specFor("ingest", "rotate"));
69
+ const appArg = args.flags.get("app");
70
+ if (!appArg) {
71
+ fail("usage: homespun ingest rotate --app <idOrSlug> --name <hookName>", "invalid_args");
72
+ }
73
+ const name = args.flags.get("name");
74
+ if (!name) {
75
+ fail("--name is required", "invalid_args");
76
+ }
77
+ const client = makeClient(args);
78
+ const appId = await resolveAppId(client, appArg);
79
+ try {
80
+ printJson(await client.rotateIngestHook(appId, name));
81
+ }
82
+ catch (e) {
83
+ failFromError(e);
84
+ }
85
+ }
@@ -5,37 +5,11 @@
5
5
  // authenticated agent, so there is exactly one key — the caller's own. Both
6
6
  // verbs therefore act ONLY on the caller's own key.
7
7
  import { assertKnownFlags } from "../argv.js";
8
+ import { specFor } from "../help-catalog.js";
8
9
  import { makeClient } from "../config.js";
9
10
  import { printJson, fail, failFromError } from "../output.js";
10
- const NO_FLAGS = [];
11
- const NO_BOOLS = [];
12
- const REVOKE_BOOLS = ["yes"];
13
- export const keyHelp = `homespun key — inspect or revoke YOUR agent's API key
14
-
15
- Usage:
16
- homespun key <verb> [options]
17
-
18
- Verbs:
19
- list Show YOUR agent's key info. The relay scopes keys to the
20
- authenticated agent — there is exactly one key per agent, your
21
- own. Prints { agent_id, name, key_prefix, created_at,
22
- last_used_at, revoked_at }.
23
-
24
- revoke Revoke YOUR OWN API key — a self-destruct. The key stops working
25
- IMMEDIATELY; every subsequent command fails until you run
26
- 'homespun agent register' again to provision a new key. The relay only
27
- allows revoking your own key. Requires --yes to confirm.
28
- Prints { revoked: true, agent_id }.
29
-
30
- Options:
31
- --yes Confirm 'key revoke' (required — it is irreversible).
32
- --url <url> Relay base URL (overrides HOMESPUN_URL).
33
- --api-key <key> Agent API key (overrides HOMESPUN_API_KEY).
34
- -h, --help Show this help.
35
-
36
- Output: stdout is machine-readable JSON.`;
37
11
  async function runKeyList(args) {
38
- assertKnownFlags(args, NO_FLAGS, NO_BOOLS, "homespun key list");
12
+ assertKnownFlags(args, ...specFor("key", "list"));
39
13
  const client = makeClient(args);
40
14
  try {
41
15
  const info = await client.listKeys();
@@ -45,8 +19,20 @@ async function runKeyList(args) {
45
19
  failFromError(e);
46
20
  }
47
21
  }
22
+ async function runKeyMint(args) {
23
+ assertKnownFlags(args, ...specFor("key", "mint"));
24
+ const client = makeClient(args);
25
+ try {
26
+ // The raw key is in this response ONCE and never again, so print it verbatim.
27
+ const minted = await client.mintKey();
28
+ printJson(minted);
29
+ }
30
+ catch (e) {
31
+ failFromError(e);
32
+ }
33
+ }
48
34
  async function runKeyRevoke(args) {
49
- assertKnownFlags(args, NO_FLAGS, REVOKE_BOOLS, "homespun key revoke");
35
+ assertKnownFlags(args, ...specFor("key", "revoke"));
50
36
  if (!args.bools.has("yes")) {
51
37
  fail("'homespun key revoke' revokes YOUR OWN API key — it stops working " +
52
38
  "immediately and is irreversible. Pass --yes to confirm.", "confirmation_required");
@@ -70,13 +56,16 @@ export async function runKey(args) {
70
56
  case "list":
71
57
  await runKeyList(args);
72
58
  break;
59
+ case "mint":
60
+ await runKeyMint(args);
61
+ break;
73
62
  case "revoke":
74
63
  await runKeyRevoke(args);
75
64
  break;
76
65
  case undefined:
77
- fail("missing verb usage: homespun key <list|revoke> (run 'homespun key --help')", "invalid_args");
66
+ fail("missing verb; usage: homespun key <list|mint|revoke> (run 'homespun key --help')", "invalid_args");
78
67
  break;
79
68
  default:
80
- fail(`unknown key verb '${sub}' expected list|revoke (run 'homespun key --help')`, "invalid_args");
69
+ fail(`unknown key verb '${sub}', expected list|mint|revoke (run 'homespun key --help')`, "invalid_args");
81
70
  }
82
71
  }
@@ -1,36 +1,10 @@
1
1
  // `homespun agent logout` — clear one (or all) saved profile(s).
2
2
  import { assertKnownFlags } from "../argv.js";
3
+ import { specFor } from "../help-catalog.js";
3
4
  import { clearStore, readStore, removeProfile, resolveProfile, } from "../store.js";
4
5
  import { printJson, fail } from "../output.js";
5
- const NO_FLAGS = [];
6
- const KNOWN_BOOLS = ["all"];
7
- export const logoutHelp = `homespun agent logout — clear a saved profile (or all of them)
8
-
9
- Usage:
10
- homespun agent logout [options]
11
-
12
- By default this clears the ACTIVE profile only (the one selected by --profile
13
- / HOMESPUN_PROFILE / the store's current_profile). The on-disk file keeps the
14
- other profiles, and 'current_profile' is unset so the next command falls back
15
- to env / default URL until another profile is selected.
16
-
17
- Pass --all to delete the whole config file (the pre-profile behaviour) — this
18
- wipes every profile, not just the active one. Idempotent — no error if there
19
- is nothing to clear.
20
-
21
- This only clears the LOCAL config. It does NOT revoke the key on the relay —
22
- keys keep working until revoked. To revoke a key server-side, use
23
- 'homespun key revoke'.
24
-
25
- Options:
26
- --profile <name> Target this profile instead of the active one.
27
- --all Delete every profile (the whole config file).
28
- -h, --help Show this help.
29
-
30
- Output (stdout, JSON):
31
- { cleared: true, profile, path } (profile=null when --all)`;
32
6
  export async function runLogout(args) {
33
- assertKnownFlags(args, NO_FLAGS, KNOWN_BOOLS, "homespun agent logout");
7
+ assertKnownFlags(args, ...specFor("agent", "logout"));
34
8
  if (args.bools.has("all")) {
35
9
  // Nuke everything — file gone, both legacy and new shape covered.
36
10
  const path = clearStore();
@@ -14,43 +14,18 @@
14
14
  // authorizable — was removed in PR 2c-1 along with the rest of the v1
15
15
  // Template subsystem.)
16
16
  import { assertKnownFlags } from "../argv.js";
17
+ import { nounSpec, renderNounHelp, specFor } from "../help-catalog.js";
17
18
  import { makeClient } from "../config.js";
18
19
  import { fail, failFromError, printJson } from "../output.js";
19
20
  import { resolveAppId } from "../resolve-app.js";
20
- export const membersHelp = `homespun members — app membership management
21
-
22
- Usage:
23
- homespun members add --app <idOrSlug> --email <email> [--role member]
24
- homespun members list --app <idOrSlug>
25
- homespun members remove --app <idOrSlug> --human <humanId>
26
-
27
- --app accepts either the app_id or its slug (resolved via GET /v1/apps?slug=
28
- when it doesn't look like a cuid).
29
-
30
- add: if a Human already exists for --email, the member row is attached
31
- immediately and the response is { member: { humanId, email, role,
32
- createdAt } }. Otherwise the relay mints a signed invite and emails a magic
33
- link, responding { ok: true, invited, expires_at }. Only "member" is a valid
34
- --role (the default); ownership transfer is not available here. Fails with
35
- a relay error (503 auth_provider_unavailable) if the relay has no email
36
- provider configured.
37
-
38
- list: returns { members: [{ humanId, email, role, createdAt }] } — the
39
- app's owner plus every attached member.
40
-
41
- remove: idempotent; also revokes the human's live sessions on this app. The
42
- app owner cannot be removed (the relay refuses with a 409 conflict).
43
-
44
- Output (JSON). Errors on stderr:
45
- {"error":{"code","message"}} with non-zero exit.`;
46
21
  export async function runMembers(args) {
47
22
  const verb = args.positionals[0];
48
23
  if ((verb === undefined || verb === "help") && args.bools.has("help")) {
49
- process.stdout.write(membersHelp + "\n");
24
+ process.stdout.write(renderNounHelp(nounSpec("members")) + "\n");
50
25
  return;
51
26
  }
52
27
  if (verb === undefined) {
53
- fail("missing verb — homespun members <add|list|remove>", "invalid_args");
28
+ fail("missing verb — homespun members <add|list|set-role|remove|roles>", "invalid_args");
54
29
  }
55
30
  const sub = {
56
31
  positionals: args.positionals.slice(1),
@@ -65,17 +40,21 @@ export async function runMembers(args) {
65
40
  return runAdd(sub);
66
41
  case "list":
67
42
  return runList(sub);
43
+ case "set-role":
44
+ return runSetRole(sub);
68
45
  case "remove":
69
46
  return runRemove(sub);
47
+ case "roles":
48
+ return runRoles(sub);
70
49
  default:
71
- fail(`unknown verb '${verb}' — homespun members <add|list|remove>`, "invalid_args");
50
+ fail(`unknown verb '${verb}' — homespun members <add|list|set-role|remove|roles>`, "invalid_args");
72
51
  }
73
52
  }
74
53
  // ---------------------------------------------------------------------------
75
54
  // add
76
55
  // ---------------------------------------------------------------------------
77
56
  async function runAdd(args) {
78
- assertKnownFlags(args, ["app", "email", "role", "url", "api-key"], ["help"], "homespun members add");
57
+ assertKnownFlags(args, ...specFor("members", "add"));
79
58
  const appArg = args.flags.get("app");
80
59
  if (!appArg) {
81
60
  fail("usage: homespun members add --app <idOrSlug> --email <email> [--role member]", "invalid_args");
@@ -104,7 +83,7 @@ async function runAdd(args) {
104
83
  // list
105
84
  // ---------------------------------------------------------------------------
106
85
  async function runList(args) {
107
- assertKnownFlags(args, ["app", "url", "api-key"], ["help"], "homespun members list");
86
+ assertKnownFlags(args, ...specFor("members", "list"));
108
87
  const appArg = args.flags.get("app");
109
88
  if (!appArg) {
110
89
  fail("usage: homespun members list --app <idOrSlug>", "invalid_args");
@@ -119,10 +98,45 @@ async function runList(args) {
119
98
  }
120
99
  }
121
100
  // ---------------------------------------------------------------------------
101
+ // set-role
102
+ // ---------------------------------------------------------------------------
103
+ async function runSetRole(args) {
104
+ assertKnownFlags(args, ...specFor("members", "set-role"));
105
+ const appArg = args.flags.get("app");
106
+ if (!appArg) {
107
+ fail("usage: homespun members set-role --app <idOrSlug> --human <humanId> (--custom-role <name> | --clear-role)", "invalid_args");
108
+ }
109
+ const humanId = args.flags.get("human");
110
+ if (!humanId) {
111
+ fail("--human is required", "invalid_args");
112
+ }
113
+ // Clearing a role is a real instruction, so it gets its own explicit flag
114
+ // rather than being spelled as an omitted or empty --custom-role: an omitted
115
+ // value must never silently wipe someone's role.
116
+ const customRole = args.flags.get("custom-role");
117
+ const clear = args.bools.has("clear-role");
118
+ if (clear && customRole !== undefined) {
119
+ fail("--custom-role and --clear-role are mutually exclusive", "invalid_args");
120
+ }
121
+ if (!clear && customRole === undefined) {
122
+ fail("one of --custom-role <name> or --clear-role is required", "invalid_args");
123
+ }
124
+ const client = makeClient(args);
125
+ const appId = await resolveAppId(client, appArg);
126
+ try {
127
+ printJson(await client.setAppMemberRole(appId, humanId, {
128
+ customRole: clear ? null : customRole,
129
+ }));
130
+ }
131
+ catch (e) {
132
+ failFromError(e);
133
+ }
134
+ }
135
+ // ---------------------------------------------------------------------------
122
136
  // remove
123
137
  // ---------------------------------------------------------------------------
124
138
  async function runRemove(args) {
125
- assertKnownFlags(args, ["app", "human", "url", "api-key"], ["help"], "homespun members remove");
139
+ assertKnownFlags(args, ...specFor("members", "remove"));
126
140
  const appArg = args.flags.get("app");
127
141
  if (!appArg) {
128
142
  fail("usage: homespun members remove --app <idOrSlug> --human <humanId>", "invalid_args");
@@ -141,3 +155,21 @@ async function runRemove(args) {
141
155
  failFromError(e);
142
156
  }
143
157
  }
158
+ // ---------------------------------------------------------------------------
159
+ // roles
160
+ // ---------------------------------------------------------------------------
161
+ async function runRoles(args) {
162
+ assertKnownFlags(args, ...specFor("members", "roles"));
163
+ const appArg = args.flags.get("app");
164
+ if (!appArg) {
165
+ fail("usage: homespun members roles --app <idOrSlug>", "invalid_args");
166
+ }
167
+ const client = makeClient(args);
168
+ const appId = await resolveAppId(client, appArg);
169
+ try {
170
+ printJson(await client.listAppRoles(appId));
171
+ }
172
+ catch (e) {
173
+ failFromError(e);
174
+ }
175
+ }