@faable/faable 3.4.1 → 4.0.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.
Files changed (35) hide show
  1. package/dist/api/FaableApi.js +49 -9
  2. package/dist/api/auth_admin.js +144 -35
  3. package/dist/api/context.js +9 -0
  4. package/dist/api/project.js +62 -0
  5. package/dist/commands/auth/accounts/index.js +76 -0
  6. package/dist/commands/auth/actions/create.js +2 -2
  7. package/dist/commands/auth/actions/get.js +3 -2
  8. package/dist/commands/auth/actions/list.js +13 -13
  9. package/dist/commands/auth/actions/update.js +2 -2
  10. package/dist/commands/auth/clients/create.js +2 -2
  11. package/dist/commands/auth/clients/get.js +2 -2
  12. package/dist/commands/auth/clients/list.js +13 -13
  13. package/dist/commands/auth/index.js +6 -3
  14. package/dist/commands/auth/logs/get.js +3 -2
  15. package/dist/commands/auth/logs/list.js +13 -13
  16. package/dist/commands/auth/options.js +5 -28
  17. package/dist/commands/auth/render.js +2 -14
  18. package/dist/commands/auth/users/get.js +5 -5
  19. package/dist/commands/auth/users/import.js +9 -2
  20. package/dist/commands/auth/users/list.js +13 -13
  21. package/dist/commands/auth/users/reinstate.js +2 -2
  22. package/dist/commands/auth/users/suspend.js +2 -2
  23. package/dist/commands/deploy/domains/list.js +10 -3
  24. package/dist/commands/deploy/inspect/deployments.js +12 -13
  25. package/dist/commands/deploy/inspect/list.js +18 -9
  26. package/dist/commands/deploy/secrets/list.js +11 -2
  27. package/dist/commands/deploy/waf/list.js +6 -2
  28. package/dist/commands/project/index.js +95 -0
  29. package/dist/commands/whoami/index.js +55 -5
  30. package/dist/index.js +28 -2
  31. package/dist/lib/ContextStore.js +30 -0
  32. package/dist/lib/listing.js +91 -0
  33. package/dist/log.js +12 -1
  34. package/package.json +1 -1
  35. package/dist/commands/auth/paging.js +0 -9
@@ -1,12 +1,13 @@
1
+ import { accounts, auth_use } from './accounts/index.js';
1
2
  import { actions } from './actions/index.js';
2
3
  import { clients } from './clients/index.js';
3
4
  import { logs } from './logs/index.js';
4
5
  import { users } from './users/index.js';
5
6
 
6
7
  // `faable auth` — management commands for a Faable Auth tenant. Auth: reuses
7
- // the `faable login` session (or FAABLE_TOKEN); tenant: --auth-url /
8
- // FAABLE_AUTH_URL (defaults to the Faable tenant host). See
9
- // src/api/auth_admin.ts.
8
+ // the `faable login` session (or FAABLE_TOKEN); tenant: --account /
9
+ // --auth-url, else the active project's (`faable auth accounts list`). See
10
+ // resolveTenant in src/api/auth_admin.ts.
10
11
  const auth = {
11
12
  command: 'auth',
12
13
  describe: 'Manage Faable Auth (users, actions, clients, audit logs)',
@@ -15,6 +16,8 @@ const auth = {
15
16
  .command(actions)
16
17
  .command(clients)
17
18
  .command(logs)
19
+ .command(accounts)
20
+ .command(auth_use)
18
21
  .demandCommand(1)
19
22
  .showHelpOnFail(false),
20
23
  handler: () => { }
@@ -1,7 +1,8 @@
1
1
  import { withAuthHints, requireAuthAdmin } from '../../../api/auth_admin.js';
2
2
  import { log } from '../../../log.js';
3
- import { json_option, tenant_options } from '../options.js';
4
- import { print_json, log_status_badge } from '../render.js';
3
+ import { tenant_options } from '../options.js';
4
+ import { log_status_badge } from '../render.js';
5
+ import { json_option, print_json } from '../../../lib/listing.js';
5
6
 
6
7
  const logs_get = {
7
8
  command: 'get <log_id>',
@@ -1,14 +1,14 @@
1
1
  import { withAuthHints, requireAuthAdmin } from '../../../api/auth_admin.js';
2
2
  import { log } from '../../../log.js';
3
- import { json_option, list_options, tenant_options } from '../options.js';
4
- import { fetch_items } from '../paging.js';
3
+ import { list_options, fetch_page, from_paginator, print_json, table_lines, print, more_hint } from '../../../lib/listing.js';
4
+ import { tenant_options } from '../options.js';
5
5
  import { compose_query, term, time_term } from '../query.js';
6
- import { print_json, log_status_badge, truncate, table_lines } from '../render.js';
6
+ import { log_status_badge, truncate } from '../render.js';
7
7
 
8
8
  const logs_list = {
9
9
  command: 'list',
10
10
  describe: 'List and filter audit logs',
11
- builder: yargs => json_option(list_options(tenant_options(yargs)))
11
+ builder: yargs => list_options(tenant_options(yargs))
12
12
  .option('query', {
13
13
  type: 'string',
14
14
  description: 'Raw FaableQL filter (combined with the flags below)'
@@ -60,15 +60,15 @@ const logs_list = {
60
60
  time_term('until', args.until)
61
61
  ], args.query);
62
62
  const api = await requireAuthAdmin(args);
63
- const { items, more } = await fetch_items(api.logList({ query, q: args.q, pageSize: args.limit }), args.all);
63
+ const page = await fetch_page(from_paginator(api.logList({ query, q: args.q })), args);
64
64
  if (args.json)
65
- return print_json(items);
66
- if (items.length === 0) {
65
+ return print_json(page);
66
+ if (page.data.length === 0) {
67
67
  log.info('📭 No audit log entries match.');
68
68
  return;
69
69
  }
70
- log.info(`📜 ${items.length} entr${items.length === 1 ? 'y' : 'ies'}:`);
71
- const rows = items.map(entry => [
70
+ log.info(`📜 ${page.data.length} entr${page.data.length === 1 ? 'y' : 'ies'}:`);
71
+ const rows = page.data.map(entry => [
72
72
  entry.createdAt ?? '-',
73
73
  entry.type ?? '-',
74
74
  log_status_badge(entry.status),
@@ -79,11 +79,11 @@ const logs_list = {
79
79
  entry.id ?? '-'
80
80
  ]);
81
81
  for (const line of table_lines(['DATE', 'TYPE', 'STATUS', 'USER', 'MESSAGE', 'ID'], rows)) {
82
- log.info(` ${line}`);
83
- }
84
- if (more) {
85
- log.info('… more results available: raise --limit, use --all, or narrow with --since/--type.');
82
+ print(line);
86
83
  }
84
+ const hint = more_hint(page);
85
+ if (hint)
86
+ log.info(hint);
87
87
  })
88
88
  };
89
89
 
@@ -1,34 +1,11 @@
1
1
  const tenant_options = (yargs) => yargs
2
- .option('auth-url', {
3
- type: 'string',
4
- description: 'Auth tenant base URL, e.g. https://<account>.auth.faable.link (env FAABLE_AUTH_URL)'
5
- })
6
2
  .option('account', {
7
3
  type: 'string',
8
- description: 'Resolve a different account id on the target host (env FAABLE_AUTH_ACCOUNT)'
9
- });
10
- const json_option = (yargs) => yargs.option('json', {
11
- type: 'boolean',
12
- default: false,
13
- description: 'Output raw JSON (for scripting)'
14
- });
15
- const list_options = (yargs) => yargs
16
- .option('limit', {
17
- type: 'number',
18
- default: 100,
19
- description: 'Max items to fetch in one page (1-200)'
20
- })
21
- .option('all', {
22
- type: 'boolean',
23
- default: false,
24
- description: 'Fetch every page (ignores --limit)'
4
+ description: "Auth tenant id (env FAABLE_AUTH_ACCOUNT). Default: the active project's tenant"
25
5
  })
26
- .check(argv => {
27
- const limit = argv.limit;
28
- if (limit !== undefined && (!Number.isInteger(limit) || limit < 1 || limit > 200)) {
29
- throw new Error('--limit must be an integer between 1 and 200');
30
- }
31
- return true;
6
+ .option('auth-url', {
7
+ type: 'string',
8
+ description: 'Auth tenant base URL, e.g. https://<slug>.auth.faable.link (env FAABLE_AUTH_URL)'
32
9
  });
33
10
 
34
- export { json_option, list_options, tenant_options };
11
+ export { tenant_options };
@@ -1,17 +1,5 @@
1
1
  // Pure rendering helpers for the `faable auth` read commands.
2
- const print_json = (data) => {
3
- process.stdout.write(JSON.stringify(data, null, 2) + '\n');
4
- };
5
- // Left-padded fixed-width table lines. Cells are stringified as-is; column
6
- // width = max(header, cells).
7
- const table_lines = (headers, rows) => {
8
- const widths = headers.map((h, i) => Math.max(h.length, ...rows.map(r => (r[i] ?? '').length)));
9
- const render = (cells) => cells
10
- .map((c, i) => (i === cells.length - 1 ? c : (c ?? '').padEnd(widths[i])))
11
- .join(' ')
12
- .trimEnd();
13
- return [render(headers), ...rows.map(render)];
14
- };
2
+ // JSON output and tables are the listing contract's (src/lib/listing.ts).
15
3
  const yes_no = (v) => (v ? '✓' : '-');
16
4
  const truncate = (s, max) => {
17
5
  if (!s)
@@ -45,4 +33,4 @@ const log_status_badge = (status) => {
45
33
  return `${STATUS_ICONS[s] ?? '⚪'} ${s}`;
46
34
  };
47
35
 
48
- export { log_status_badge, print_json, table_lines, truncate, when, yes_no };
36
+ export { log_status_badge, truncate, when, yes_no };
@@ -1,8 +1,8 @@
1
1
  import { withAuthHints, requireAuthAdmin } from '../../../api/auth_admin.js';
2
2
  import { log } from '../../../log.js';
3
- import { json_option, tenant_options } from '../options.js';
4
- import { fetch_items } from '../paging.js';
5
- import { print_json, yes_no, when } from '../render.js';
3
+ import { tenant_options } from '../options.js';
4
+ import { json_option, fetch_page, from_paginator, print_json } from '../../../lib/listing.js';
5
+ import { yes_no, when } from '../render.js';
6
6
 
7
7
  // Identity rows carry the RAW provider access/refresh tokens — strip them
8
8
  // before anything (json output included) can print one.
@@ -28,8 +28,8 @@ const users_get = {
28
28
  // rarely matches the GitHub handle that owns the deployed repos).
29
29
  // Best-effort: a tenant where the session can't read /identity still
30
30
  // renders the user card.
31
- const identities = await fetch_items(api.identityList({ query: `user:${args.user_id}` }), true)
32
- .then(r => r.items.map(i => sanitize_identity(i)))
31
+ const identities = await fetch_page(from_paginator(api.identityList({ query: `user:${args.user_id}` })), { limit: 200, all: true })
32
+ .then(r => r.data.map(i => sanitize_identity(i)))
33
33
  .catch(() => []);
34
34
  // Resolve connection ids to names ("github") once per distinct id.
35
35
  const connection_names = new Map();
@@ -2,8 +2,8 @@ import { readFile } from 'node:fs/promises';
2
2
  import prompts from 'prompts';
3
3
  import { withAuthHints, requireAuthAdmin } from '../../../api/auth_admin.js';
4
4
  import { log } from '../../../log.js';
5
- import { json_option, tenant_options } from '../options.js';
6
- import { print_json } from '../render.js';
5
+ import { tenant_options } from '../options.js';
6
+ import { json_option, print_json } from '../../../lib/listing.js';
7
7
  import { SOURCES, ADAPTERS, SOURCE_FORMAT } from './import/adapters.js';
8
8
  import { parse_records } from './import/records.js';
9
9
 
@@ -149,6 +149,8 @@ const users_import = {
149
149
  email: r.email,
150
150
  status: r.status,
151
151
  user_id: r.user_id,
152
+ skipped_identities: r
153
+ .skipped_identities,
152
154
  error: r.error
153
155
  });
154
156
  }
@@ -169,6 +171,11 @@ const users_import = {
169
171
  print_json({ dry_run, summary, results });
170
172
  }
171
173
  else {
174
+ const without = results.filter(r => r.skipped_identities?.length);
175
+ if (without.length) {
176
+ const names = [...new Set(without.flatMap(r => r.skipped_identities))];
177
+ log.warn(`⚠️ ${without.length} user(s) imported without their ${names.join(', ')} identity: this tenant has no connection by that name. Create it and re-run with --update-existing, or map it with --map-connection.`);
178
+ }
172
179
  for (const r of results.filter(r => r.status === 'error')) {
173
180
  log.error(`❌ record ${r.record}${r.email ? ` (${r.email})` : ''}: ${r.error?.code} — ${r.error?.message}`);
174
181
  }
@@ -1,14 +1,14 @@
1
1
  import { withAuthHints, requireAuthAdmin } from '../../../api/auth_admin.js';
2
2
  import { log } from '../../../log.js';
3
- import { json_option, list_options, tenant_options } from '../options.js';
3
+ import { list_options, fetch_page, from_paginator, print_json, table_lines, print, more_hint } from '../../../lib/listing.js';
4
+ import { tenant_options } from '../options.js';
4
5
  import { compose_query } from '../query.js';
5
- import { fetch_items } from '../paging.js';
6
- import { print_json, truncate, yes_no, when, table_lines } from '../render.js';
6
+ import { truncate, yes_no, when } from '../render.js';
7
7
 
8
8
  const users_list = {
9
9
  command: 'list',
10
10
  describe: 'List and filter users',
11
- builder: yargs => json_option(list_options(tenant_options(yargs)))
11
+ builder: yargs => list_options(tenant_options(yargs))
12
12
  .option('query', {
13
13
  type: 'string',
14
14
  description: 'FaableQL filter, e.g. "suspended:true email_verified:false" (fields: email, name, phone, suspended, email_verified, country_iso, locale, last_ip)'
@@ -27,15 +27,15 @@ const users_list = {
27
27
  handler: withAuthHints(async (args) => {
28
28
  const api = await requireAuthAdmin(args);
29
29
  const query = compose_query([args.suspended !== undefined && `suspended:${args.suspended}`], args.query);
30
- const { items, more } = await fetch_items(api.userList({ query, q: args.q, pageSize: args.limit }), args.all);
30
+ const page = await fetch_page(from_paginator(api.userList({ query, q: args.q })), args);
31
31
  if (args.json)
32
- return print_json(items);
33
- if (items.length === 0) {
32
+ return print_json(page);
33
+ if (page.data.length === 0) {
34
34
  log.info('📭 No users match.');
35
35
  return;
36
36
  }
37
- log.info(`👥 ${items.length} user(s):`);
38
- const rows = items.map(u => [
37
+ log.info(`👥 ${page.data.length} user(s):`);
38
+ const rows = page.data.map(u => [
39
39
  u.id ?? '-',
40
40
  truncate(u.email, 32),
41
41
  truncate(u.name, 24),
@@ -44,11 +44,11 @@ const users_list = {
44
44
  when(u.last_login)
45
45
  ]);
46
46
  for (const line of table_lines(['ID', 'EMAIL', 'NAME', 'VERIFIED', 'SUSPENDED', 'LAST LOGIN'], rows)) {
47
- log.info(` ${line}`);
48
- }
49
- if (more) {
50
- log.info('… more results available: raise --limit, use --all, or refine the filter.');
47
+ print(line);
51
48
  }
49
+ const hint = more_hint(page);
50
+ if (hint)
51
+ log.info(hint);
52
52
  })
53
53
  };
54
54
 
@@ -1,8 +1,8 @@
1
1
  import prompts from 'prompts';
2
2
  import { withAuthHints, requireAuthAdmin } from '../../../api/auth_admin.js';
3
3
  import { log } from '../../../log.js';
4
- import { json_option, tenant_options } from '../options.js';
5
- import { print_json } from '../render.js';
4
+ import { tenant_options } from '../options.js';
5
+ import { json_option, print_json } from '../../../lib/listing.js';
6
6
  import { wants_stdin, read_stdin, parse_user_ids } from './ids.js';
7
7
 
8
8
  const users_reinstate = {
@@ -1,8 +1,8 @@
1
1
  import prompts from 'prompts';
2
2
  import { withAuthHints, requireAuthAdmin } from '../../../api/auth_admin.js';
3
3
  import { log } from '../../../log.js';
4
- import { json_option, tenant_options } from '../options.js';
5
- import { print_json } from '../render.js';
4
+ import { tenant_options } from '../options.js';
5
+ import { json_option, print_json } from '../../../lib/listing.js';
6
6
  import { wants_stdin, read_stdin, parse_user_ids } from './ids.js';
7
7
 
8
8
  const users_suspend = {
@@ -1,4 +1,5 @@
1
1
  import { requireApi } from '../../../api/context.js';
2
+ import { fetch_page, print_json, print, more_hint, list_options } from '../../../lib/listing.js';
2
3
  import { log } from '../../../log.js';
3
4
  import { resolve_app_id } from '../resolve_app_id.js';
4
5
  import { dns_badge, cname_target } from './format.js';
@@ -6,7 +7,7 @@ import { dns_badge, cname_target } from './format.js';
6
7
  const domains_list = {
7
8
  command: 'list',
8
9
  describe: 'List custom domains of the app',
9
- builder: yargs => yargs
10
+ builder: yargs => list_options(yargs)
10
11
  .option('app', {
11
12
  alias: 'a',
12
13
  type: 'string',
@@ -18,7 +19,10 @@ const domains_list = {
18
19
  const ctx = await requireApi();
19
20
  const app_id = await resolve_app_id(args.app, ctx.appId, ctx.api);
20
21
  const app = await ctx.api.getApp(app_id);
21
- const domains = await ctx.api.listDomains(app_id, app.team);
22
+ const page = await fetch_page(p => ctx.api.listDomainsPage(app_id, app.team, p), args);
23
+ if (args.json)
24
+ return print_json(page);
25
+ const domains = page.data;
22
26
  if (domains.length === 0) {
23
27
  log.info(`🌐 No custom domains for ${app.name} (${app_id}).`);
24
28
  log.info(`Add one with: faable deploy domains add <yourdomain.com>. The app is always live at https://${app.url}.`);
@@ -28,8 +32,11 @@ const domains_list = {
28
32
  const width = Math.max(...domains.map(d => d.fqdn.length));
29
33
  for (const domain of domains) {
30
34
  const tls = domain.tls ? 'tls' : 'no-tls';
31
- log.info(` ${domain.fqdn.padEnd(width)} ${dns_badge(domain)} (${tls})`);
35
+ print(` ${domain.fqdn.padEnd(width)} ${dns_badge(domain)} (${tls})`);
32
36
  }
37
+ const hint = more_hint(page);
38
+ if (hint)
39
+ log.info(hint);
33
40
  const unverified = domains.filter(d => !d.verified);
34
41
  if (unverified.length > 0) {
35
42
  log.info(``);
@@ -1,4 +1,5 @@
1
1
  import { requireApi } from '../../../api/context.js';
2
+ import { fetch_page, print_json, print, more_hint, list_options } from '../../../lib/listing.js';
2
3
  import { log } from '../../../log.js';
3
4
  import { resolve_app_id } from '../resolve_app_id.js';
4
5
  import { deployment_row } from './format.js';
@@ -6,34 +7,32 @@ import { deployment_row } from './format.js';
6
7
  const deployments = {
7
8
  command: 'deployments',
8
9
  describe: 'List recent deployments of the app',
9
- builder: yargs => yargs
10
+ builder: yargs => list_options(yargs, { default_limit: 10 })
10
11
  .option('app', {
11
12
  alias: 'a',
12
13
  type: 'string',
13
14
  description: 'App Identifier (defaults to the linked app)'
14
- })
15
- .option('limit', {
16
- alias: 'n',
17
- type: 'number',
18
- default: 10,
19
- description: 'How many to show'
20
15
  })
21
16
  .showHelpOnFail(false),
22
17
  handler: async (args) => {
23
18
  const ctx = await requireApi();
24
19
  const app_id = await resolve_app_id(args.app, ctx.appId, ctx.api);
25
20
  const app = await ctx.api.getApp(app_id);
26
- const rows = await ctx.api.listDeployments(app_id, app.team);
27
- if (rows.length === 0) {
21
+ const page = await fetch_page(p => ctx.api.listDeploymentsPage(app_id, app.team, p), args);
22
+ if (args.json)
23
+ return print_json(page);
24
+ if (page.data.length === 0) {
28
25
  log.info(`📭 ${app.name} has no deployments yet.`);
29
26
  return;
30
27
  }
31
- const shown = rows.slice(0, args.limit ?? 10);
32
- log.info(`🚀 Last ${shown.length} deployment(s) of ${app.name}:`);
33
- for (const d of shown) {
28
+ log.info(`🚀 Last ${page.data.length} deployment(s) of ${app.name}:`);
29
+ for (const d of page.data) {
34
30
  const live = d.id === app.status?.deployment ? ' ← live' : '';
35
- log.info(` ${deployment_row(d)}${live}`);
31
+ print(` ${deployment_row(d)}${live}`);
36
32
  }
33
+ const hint = more_hint(page);
34
+ if (hint)
35
+ log.info(hint);
37
36
  log.info(`Full record of one: faable deploy inspect <deployment_id>`);
38
37
  }
39
38
  };
@@ -1,23 +1,32 @@
1
1
  import { requireApi } from '../../../api/context.js';
2
+ import { fetch_page, print_json, print, more_hint, list_options } from '../../../lib/listing.js';
2
3
  import { log } from '../../../log.js';
3
4
  import { phase_badge } from './format.js';
4
5
 
5
6
  const apps_list = {
6
7
  command: 'list',
7
- describe: 'List your apps',
8
- builder: yargs => yargs.showHelpOnFail(false),
9
- handler: async () => {
8
+ describe: 'List the apps of the active project',
9
+ builder: yargs => list_options(yargs).showHelpOnFail(false),
10
+ handler: async (args) => {
10
11
  const ctx = await requireApi();
11
- const apps = await ctx.api.list();
12
- if (apps.length === 0) {
12
+ // Scoped to the active project when there is one (--project,
13
+ // FAABLE_PROJECT, `faable project use`); every app you can see otherwise.
14
+ const page = await fetch_page(p => ctx.api.listApps(p), args);
15
+ if (args.json)
16
+ return print_json(page);
17
+ if (page.data.length === 0) {
13
18
  log.info(`📭 No apps yet. Create one in the dashboard (https://dashboard.faable.com) and link your repo.`);
14
19
  return;
15
20
  }
16
- log.info(`📦 ${apps.length} app(s):`);
17
- const width = Math.max(...apps.map(a => a.name.length));
18
- for (const app of apps) {
19
- log.info(` ${app.name.padEnd(width)} ${phase_badge(app.status?.phase)} ${app.id} https://${app.url}`);
21
+ const scope = ctx.api.project ? ` in ${ctx.api.project}` : '';
22
+ log.info(`📦 ${page.data.length} app(s)${scope}:`);
23
+ const width = Math.max(...page.data.map(a => a.name.length));
24
+ for (const app of page.data) {
25
+ print(` ${app.name.padEnd(width)} ${phase_badge(app.status?.phase)} ${app.id} https://${app.url}`);
20
26
  }
27
+ const hint = more_hint(page);
28
+ if (hint)
29
+ log.info(hint);
21
30
  }
22
31
  };
23
32
 
@@ -1,4 +1,5 @@
1
1
  import { requireApi } from '../../../api/context.js';
2
+ import { print_json, as_list, print, json_option } from '../../../lib/listing.js';
2
3
  import { log } from '../../../log.js';
3
4
  import { resolve_app_id } from '../resolve_app_id.js';
4
5
  import { managed_name, managed_warning } from './managed_names.js';
@@ -7,7 +8,7 @@ import { mask_value } from './mask.js';
7
8
  const secrets_list = {
8
9
  command: 'list',
9
10
  describe: 'List app secrets (values masked by default)',
10
- builder: yargs => yargs
11
+ builder: yargs => json_option(yargs)
11
12
  .option('app', {
12
13
  alias: 'a',
13
14
  type: 'string',
@@ -25,6 +26,14 @@ const secrets_list = {
25
26
  const ctx = await requireApi();
26
27
  const app_id = await resolve_app_id(args.app, ctx.appId, ctx.api);
27
28
  const secrets = await ctx.api.getAppSecrets(app_id);
29
+ // The whole set in one response (the api doesn't page secrets). Values
30
+ // stay masked unless --show, in JSON as on screen.
31
+ if (args.json) {
32
+ return print_json(as_list(secrets.map(s => ({
33
+ ...s,
34
+ value: args.show ? s.value : mask_value(s.value)
35
+ }))));
36
+ }
28
37
  if (secrets.length === 0) {
29
38
  log.info(`🔐 No secrets set for ${app_id}.`);
30
39
  return;
@@ -45,7 +54,7 @@ const secrets_list = {
45
54
  : managed.kind === 'reserved'
46
55
  ? ' ⚠️ reserved, ignored at deploy'
47
56
  : ' ⚠️ overrides a platform default';
48
- log.info(` ${secret.name.padEnd(width)} ${value}${origin}${note}`);
57
+ print(` ${secret.name.padEnd(width)} ${value}${origin}${note}`);
49
58
  }
50
59
  if (!args.show) {
51
60
  log.info(`Use --show to reveal full values.`);
@@ -1,4 +1,5 @@
1
1
  import { requireApi } from '../../../api/context.js';
2
+ import { print_json, print, json_option } from '../../../lib/listing.js';
2
3
  import { log } from '../../../log.js';
3
4
  import { resolve_app_id } from '../resolve_app_id.js';
4
5
  import { format_waf } from './format.js';
@@ -6,7 +7,7 @@ import { format_waf } from './format.js';
6
7
  const waf_list = {
7
8
  command: 'list',
8
9
  describe: 'Show the WAF rules in effect for the app',
9
- builder: yargs => yargs
10
+ builder: yargs => json_option(yargs)
10
11
  .option('app', {
11
12
  alias: 'a',
12
13
  type: 'string',
@@ -19,10 +20,13 @@ const waf_list = {
19
20
  const app_id = await resolve_app_id(args.app, ctx.appId, ctx.api);
20
21
  const app = await ctx.api.getApp(app_id);
21
22
  const waf = await ctx.api.getAppWaf(app_id);
23
+ // Not a list: the app's WAF is one document (toggle, profiles, rules).
24
+ if (args.json)
25
+ return print_json(waf);
22
26
  log.info(`🛡️ WAF for ${app.name} (${app_id})`);
23
27
  log.info(``);
24
28
  for (const line of format_waf(waf))
25
- log.info(line);
29
+ print(line);
26
30
  }
27
31
  };
28
32
 
@@ -0,0 +1,95 @@
1
+ import { subject, requireSessionToken } from '../../api/auth_admin.js';
2
+ import { requireApi } from '../../api/context.js';
3
+ import { resolveProjectRef, configuredProject, requireProject, SOURCE_LABEL } from '../../api/project.js';
4
+ import { ContextStore } from '../../lib/ContextStore.js';
5
+ import { fetch_page, print_json, table_lines, print, more_hint, list_options, json_option } from '../../lib/listing.js';
6
+ import { log } from '../../log.js';
7
+
8
+ const project_list = {
9
+ command: 'list',
10
+ describe: 'List your projects',
11
+ builder: yargs => list_options(yargs)
12
+ .option('q', {
13
+ type: 'string',
14
+ description: 'Search by name, slug or description'
15
+ })
16
+ .option('mine', {
17
+ type: 'boolean',
18
+ default: false,
19
+ description: 'Only the projects you own (a staff session otherwise lists every project)'
20
+ })
21
+ .showHelpOnFail(false),
22
+ handler: async (args) => {
23
+ const { api } = await requireApi();
24
+ const user_id = args.mine ? subject(await requireSessionToken()) : undefined;
25
+ if (args.mine && !user_id) {
26
+ throw new Error('--mine needs a `faable login` session (or FAABLE_TOKEN).');
27
+ }
28
+ const page = await fetch_page(p => api.listProjects({ ...p, q: args.q, user_id }), args);
29
+ if (args.json)
30
+ return print_json(page);
31
+ if (page.data.length === 0) {
32
+ log.info('📭 No projects.');
33
+ return;
34
+ }
35
+ const active = api.project;
36
+ log.info(`📁 ${page.data.length} project(s):`);
37
+ for (const line of table_lines(['', 'ID', 'NAME', 'SLUG'], page.data.map(p => [p.id === active ? '*' : '', p.id, p.name, p.slug ?? '-']))) {
38
+ print(line);
39
+ }
40
+ const hint = more_hint(page);
41
+ if (hint)
42
+ log.info(hint);
43
+ }
44
+ };
45
+ const project_use = {
46
+ command: 'use <project>',
47
+ describe: 'Set the active project (id, name or slug)',
48
+ builder: yargs => yargs
49
+ .positional('project', { type: 'string', demandOption: true })
50
+ .example('$0 project use project_6a8ebd6160324d4631c12edc', 'By id')
51
+ .example('$0 project use "Faable Staff"', 'By name')
52
+ .showHelpOnFail(false),
53
+ handler: async (args) => {
54
+ const { api } = await requireApi();
55
+ const id = await resolveProjectRef(api, args.project);
56
+ // Fetching it is the membership check: the api 404s a project you can't see.
57
+ const project = await api.getProject(id);
58
+ await new ContextStore().update(ctx => ({ ...ctx, project: project.id }));
59
+ log.info(`✅ Active project: ${project.name} (${project.id})`);
60
+ }
61
+ };
62
+ const project_current = {
63
+ command: 'current',
64
+ describe: 'Show the active project and where it comes from',
65
+ builder: yargs => json_option(yargs).showHelpOnFail(false),
66
+ handler: async (args) => {
67
+ const { api } = await requireApi();
68
+ const configured = await configuredProject();
69
+ const { id, source } = await requireProject(api);
70
+ const project = await api.getProject(id);
71
+ const out = {
72
+ id: project.id,
73
+ name: project.name,
74
+ slug: project.slug ?? null,
75
+ source: configured?.source ?? source
76
+ };
77
+ if (args.json)
78
+ return print_json(out);
79
+ print(`${project.name} (${project.id})`);
80
+ log.info(`from ${SOURCE_LABEL[out.source]}`);
81
+ }
82
+ };
83
+ const project = {
84
+ command: 'project',
85
+ describe: 'Choose the project the CLI acts on (list, use, current)',
86
+ builder: yargs => yargs
87
+ .command(project_list)
88
+ .command(project_use)
89
+ .command(project_current)
90
+ .demandCommand(1)
91
+ .showHelpOnFail(false),
92
+ handler: () => { }
93
+ };
94
+
95
+ export { project };