@faable/faable 3.4.2 → 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.
- package/dist/api/FaableApi.js +49 -9
- package/dist/api/auth_admin.js +144 -35
- package/dist/api/context.js +9 -0
- package/dist/api/project.js +62 -0
- package/dist/commands/auth/accounts/index.js +76 -0
- package/dist/commands/auth/actions/create.js +2 -2
- package/dist/commands/auth/actions/get.js +3 -2
- package/dist/commands/auth/actions/list.js +13 -13
- package/dist/commands/auth/actions/update.js +2 -2
- package/dist/commands/auth/clients/create.js +2 -2
- package/dist/commands/auth/clients/get.js +2 -2
- package/dist/commands/auth/clients/list.js +13 -13
- package/dist/commands/auth/index.js +6 -3
- package/dist/commands/auth/logs/get.js +3 -2
- package/dist/commands/auth/logs/list.js +13 -13
- package/dist/commands/auth/options.js +5 -28
- package/dist/commands/auth/render.js +2 -14
- package/dist/commands/auth/users/get.js +5 -5
- package/dist/commands/auth/users/import.js +2 -2
- package/dist/commands/auth/users/list.js +13 -13
- package/dist/commands/auth/users/reinstate.js +2 -2
- package/dist/commands/auth/users/suspend.js +2 -2
- package/dist/commands/deploy/domains/list.js +10 -3
- package/dist/commands/deploy/inspect/deployments.js +12 -13
- package/dist/commands/deploy/inspect/list.js +18 -9
- package/dist/commands/deploy/secrets/list.js +11 -2
- package/dist/commands/deploy/waf/list.js +6 -2
- package/dist/commands/project/index.js +95 -0
- package/dist/commands/whoami/index.js +55 -5
- package/dist/index.js +28 -2
- package/dist/lib/ContextStore.js +30 -0
- package/dist/lib/listing.js +91 -0
- package/dist/log.js +12 -1
- package/package.json +1 -1
- package/dist/commands/auth/paging.js +0 -9
|
@@ -1,12 +1,41 @@
|
|
|
1
1
|
import { log } from '../../log.js';
|
|
2
2
|
import { getMe } from '../../api/auth.js';
|
|
3
|
+
import { resolveTenant } from '../../api/auth_admin.js';
|
|
4
|
+
import { FaableApi } from '../../api/FaableApi.js';
|
|
5
|
+
import { SOURCE_LABEL, requireProject } from '../../api/project.js';
|
|
3
6
|
import { loadLiveCredentials } from '../../api/session.js';
|
|
7
|
+
import { bearer_strategy } from '../../api/strategies/bearer.strategy.js';
|
|
4
8
|
import { CredentialsStore } from '../../lib/CredentialsStore.js';
|
|
9
|
+
import { print_json, print, json_option } from '../../lib/listing.js';
|
|
5
10
|
|
|
11
|
+
// Who you are, and what the CLI acts on: the active project and the Auth
|
|
12
|
+
// tenant `faable auth` would manage. The context is best-effort — a missing
|
|
13
|
+
// project is something to show, not a reason to fail `whoami`.
|
|
14
|
+
const describeContext = async (token) => {
|
|
15
|
+
const api = FaableApi.create({ authStrategy: bearer_strategy, auth: { token } });
|
|
16
|
+
let project;
|
|
17
|
+
let tenant;
|
|
18
|
+
try {
|
|
19
|
+
const { id, source } = await requireProject(api);
|
|
20
|
+
const p = await api.getProject(id).catch(() => undefined);
|
|
21
|
+
project = { id, name: p?.name, source };
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
project = { error: e.message };
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
tenant = await resolveTenant({}, api);
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
tenant = { error: e.message };
|
|
31
|
+
}
|
|
32
|
+
return { project, tenant };
|
|
33
|
+
};
|
|
6
34
|
const whoami = {
|
|
7
35
|
command: "whoami",
|
|
8
|
-
describe: "
|
|
9
|
-
|
|
36
|
+
describe: "Show the logged in user, the active project and its Auth tenant",
|
|
37
|
+
builder: (yargs) => json_option(yargs),
|
|
38
|
+
handler: async (args) => {
|
|
10
39
|
const store = new CredentialsStore();
|
|
11
40
|
// Auto-refreshes an expired token via the stored refresh_token.
|
|
12
41
|
const config = await loadLiveCredentials(store);
|
|
@@ -16,22 +45,43 @@ const whoami = {
|
|
|
16
45
|
// API-key sessions can't be introspected at the auth server's /me; fall
|
|
17
46
|
// back to the email captured at login time.
|
|
18
47
|
if (config?.apikey && config.email) {
|
|
19
|
-
|
|
48
|
+
if (args.json)
|
|
49
|
+
return print_json({ email: config.email, credential: "apikey" });
|
|
50
|
+
print(`Logged in as: ${config.email} (API key)`);
|
|
20
51
|
return;
|
|
21
52
|
}
|
|
22
53
|
log.error("❌ Not logged in. Run 'faable login' first.");
|
|
23
54
|
process.exit(1);
|
|
24
55
|
}
|
|
56
|
+
let me;
|
|
25
57
|
try {
|
|
26
58
|
// Validate against the Auth server (it issued the token); the deploy API
|
|
27
59
|
// has no /me route.
|
|
28
|
-
|
|
29
|
-
log.info(`Logged in as: ${me.email}`);
|
|
60
|
+
me = await getMe(token);
|
|
30
61
|
}
|
|
31
62
|
catch {
|
|
32
63
|
log.error("❌ Not logged in or session expired");
|
|
33
64
|
process.exit(1);
|
|
34
65
|
}
|
|
66
|
+
const { project, tenant } = await describeContext(token);
|
|
67
|
+
if (args.json) {
|
|
68
|
+
return print_json({ email: me.email, credential: "token", project, auth_tenant: tenant });
|
|
69
|
+
}
|
|
70
|
+
print(`Logged in as: ${me.email}`);
|
|
71
|
+
if ("error" in project) {
|
|
72
|
+
print(`Project: none — faable project use <id|name>`);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
const name = project.name ? `${project.name} (${project.id})` : project.id;
|
|
76
|
+
print(`Project: ${name} [${SOURCE_LABEL[project.source]}]`);
|
|
77
|
+
}
|
|
78
|
+
if ("error" in tenant) {
|
|
79
|
+
print(`Auth tenant: none — ${tenant.error}`);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
const parts = [tenant.name, tenant.account && `(${tenant.account})`, tenant.domain];
|
|
83
|
+
print(`Auth tenant: ${parts.filter(Boolean).join(" ")}`);
|
|
84
|
+
}
|
|
35
85
|
},
|
|
36
86
|
};
|
|
37
87
|
|
package/dist/index.js
CHANGED
|
@@ -5,13 +5,16 @@ import { deploy } from './commands/deploy/index.js';
|
|
|
5
5
|
import { link_deprecated } from './commands/link/index.js';
|
|
6
6
|
import { login } from './commands/login/index.js';
|
|
7
7
|
import { logout } from './commands/logout/index.js';
|
|
8
|
+
import { project } from './commands/project/index.js';
|
|
8
9
|
import { upgrade } from './commands/upgrade/index.js';
|
|
9
10
|
import { whoami } from './commands/whoami/index.js';
|
|
10
11
|
import { version } from './config.js';
|
|
11
12
|
import { Configuration } from './lib/Configuration.js';
|
|
13
|
+
import { setProjectFlag } from './api/project.js';
|
|
12
14
|
import { notifyIfUpdateAvailable } from './lib/UpdateChecker.js';
|
|
13
15
|
import { log } from './log.js';
|
|
14
16
|
|
|
17
|
+
const wants_json = () => process.argv.includes('--json');
|
|
15
18
|
// yargs re-runs before-validation middlewares once per nested command level
|
|
16
19
|
// (`deploy secrets` = 2 runs), so keep the banner and update check to one.
|
|
17
20
|
let banner_shown = false;
|
|
@@ -27,7 +30,8 @@ yg.scriptName('faable')
|
|
|
27
30
|
// update-check notice). Same for `auth users export` without a file,
|
|
28
31
|
// whose stdout IS the export.
|
|
29
32
|
const exports_to_stdout = argv._.slice(0, 3).join(' ') === 'auth users export' && !argv.file;
|
|
30
|
-
|
|
33
|
+
// Nor when nobody is watching stderr (a script, the MCP server).
|
|
34
|
+
if (argv.json || exports_to_stdout || !process.stderr.isTTY)
|
|
31
35
|
return;
|
|
32
36
|
log.info(`Faable CLI ${version}`);
|
|
33
37
|
// `upgrade` does its own (forced) check
|
|
@@ -39,8 +43,15 @@ yg.scriptName('faable')
|
|
|
39
43
|
alias: 'config',
|
|
40
44
|
description: 'Path to the local `faable.json` file',
|
|
41
45
|
string: true
|
|
46
|
+
})
|
|
47
|
+
.option('p', {
|
|
48
|
+
alias: 'project',
|
|
49
|
+
description: 'Project to act on, id or name (env FAABLE_PROJECT; default: `faable project use`)',
|
|
50
|
+
string: true,
|
|
51
|
+
global: true
|
|
42
52
|
})
|
|
43
53
|
.middleware(function (argv) {
|
|
54
|
+
setProjectFlag(argv.project);
|
|
44
55
|
if (argv.config) {
|
|
45
56
|
Configuration.instance().setConfigFile(argv.config, {
|
|
46
57
|
ignoreWarnings: false
|
|
@@ -52,6 +63,7 @@ yg.scriptName('faable')
|
|
|
52
63
|
}, true)
|
|
53
64
|
.command(deploy)
|
|
54
65
|
.command(auth)
|
|
66
|
+
.command(project)
|
|
55
67
|
.command(login)
|
|
56
68
|
.command(logout)
|
|
57
69
|
.command(whoami)
|
|
@@ -65,7 +77,21 @@ yg.scriptName('faable')
|
|
|
65
77
|
.help()
|
|
66
78
|
.fail(function (msg, err) {
|
|
67
79
|
if (err) {
|
|
68
|
-
|
|
80
|
+
// With --json the caller is a program: the error as JSON too, on
|
|
81
|
+
// stderr, so it can tell a 404 from a 403 without parsing prose.
|
|
82
|
+
if (wants_json()) {
|
|
83
|
+
const e = err;
|
|
84
|
+
process.stderr.write(JSON.stringify({
|
|
85
|
+
error: {
|
|
86
|
+
message: e.message,
|
|
87
|
+
...(e.code ? { code: e.code } : {}),
|
|
88
|
+
...(e.status ? { status: e.status } : {})
|
|
89
|
+
}
|
|
90
|
+
}) + '\n');
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
log.error(`❌ ${err.message}`);
|
|
94
|
+
}
|
|
69
95
|
process.exit(1);
|
|
70
96
|
return;
|
|
71
97
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path__default from 'path';
|
|
4
|
+
|
|
5
|
+
class ContextStore {
|
|
6
|
+
faable_home;
|
|
7
|
+
constructor(faable_home = path__default.join(os.homedir(), '.faable')) {
|
|
8
|
+
this.faable_home = faable_home;
|
|
9
|
+
}
|
|
10
|
+
get path() {
|
|
11
|
+
return path__default.join(this.faable_home, 'config.json');
|
|
12
|
+
}
|
|
13
|
+
async load() {
|
|
14
|
+
try {
|
|
15
|
+
return (await fs.readJSON(this.path)) || {};
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async update(patch) {
|
|
22
|
+
const next = patch(await this.load());
|
|
23
|
+
await fs.ensureDir(this.faable_home);
|
|
24
|
+
await fs.writeJSON(this.path, next, { spaces: 2 });
|
|
25
|
+
await fs.chmod(this.path, 0o600);
|
|
26
|
+
return next;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { ContextStore };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// One listing contract for every `faable … list`, modelled on Stripe's CLI so
|
|
2
|
+
// a script — or the MCP server that will drive this CLI — learns it once:
|
|
3
|
+
//
|
|
4
|
+
// --limit N one page of up to N items (default 100, max 200)
|
|
5
|
+
// --starting-after CUR the page after CUR (the previous page's next_cursor)
|
|
6
|
+
// --all walk every page
|
|
7
|
+
// --json {"object":"list","data":[…],"has_more",…,"next_cursor"}
|
|
8
|
+
//
|
|
9
|
+
// The servers page by cursor (`pageSize` + `next`, arch/api/pagination.md);
|
|
10
|
+
// this is the translation. There is no --ending-before: the cursor only runs
|
|
11
|
+
// forward.
|
|
12
|
+
const MAX_PAGE_SIZE = 200;
|
|
13
|
+
const DEFAULT_PAGE_SIZE = 100;
|
|
14
|
+
const json_option = (yargs) => yargs.option('json', {
|
|
15
|
+
type: 'boolean',
|
|
16
|
+
default: false,
|
|
17
|
+
description: 'Output JSON on stdout (for scripting)'
|
|
18
|
+
});
|
|
19
|
+
const list_options = (yargs, { default_limit = DEFAULT_PAGE_SIZE } = {}) => json_option(yargs)
|
|
20
|
+
.option('limit', {
|
|
21
|
+
alias: 'n',
|
|
22
|
+
type: 'number',
|
|
23
|
+
default: default_limit,
|
|
24
|
+
description: `Items per page (1-${MAX_PAGE_SIZE})`
|
|
25
|
+
})
|
|
26
|
+
.option('starting-after', {
|
|
27
|
+
alias: 'cursor',
|
|
28
|
+
type: 'string',
|
|
29
|
+
description: 'Cursor to continue from (the next_cursor of the previous page)'
|
|
30
|
+
})
|
|
31
|
+
.option('all', {
|
|
32
|
+
type: 'boolean',
|
|
33
|
+
default: false,
|
|
34
|
+
description: 'Fetch every page'
|
|
35
|
+
})
|
|
36
|
+
.check(argv => {
|
|
37
|
+
const limit = argv.limit;
|
|
38
|
+
if (limit !== undefined &&
|
|
39
|
+
(!Number.isInteger(limit) || limit < 1 || limit > MAX_PAGE_SIZE)) {
|
|
40
|
+
throw new Error(`--limit must be an integer between 1 and ${MAX_PAGE_SIZE}`);
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
});
|
|
44
|
+
// A page of the server's `{ results, next }` from the listing flags. With
|
|
45
|
+
// --all the cursor is walked here rather than by the SDK paginator's `.all()`,
|
|
46
|
+
// which gives up at 100 pages.
|
|
47
|
+
const fetch_page = async (fetch, args) => {
|
|
48
|
+
const pageSize = args.all ? MAX_PAGE_SIZE : args.limit;
|
|
49
|
+
let next = args.startingAfter || undefined;
|
|
50
|
+
const data = [];
|
|
51
|
+
do {
|
|
52
|
+
const page = await fetch({ pageSize, ...(next ? { next } : {}) });
|
|
53
|
+
data.push(...page.results);
|
|
54
|
+
const following = page.next || undefined;
|
|
55
|
+
if (following && following === next)
|
|
56
|
+
throw new Error('Bad next cursor');
|
|
57
|
+
next = following;
|
|
58
|
+
} while (args.all && next);
|
|
59
|
+
return as_list(data, next ?? null);
|
|
60
|
+
};
|
|
61
|
+
const from_paginator = (listing) => ({ pageSize, next }) => listing.pass({ pageSize: String(pageSize), ...(next ? { cursor: next } : {}) });
|
|
62
|
+
const as_list = (data, next_cursor = null) => ({
|
|
63
|
+
object: 'list',
|
|
64
|
+
data,
|
|
65
|
+
has_more: !!next_cursor,
|
|
66
|
+
next_cursor
|
|
67
|
+
});
|
|
68
|
+
// Data goes to stdout, untouched by the logger (which writes to stderr).
|
|
69
|
+
const print = (line = '') => {
|
|
70
|
+
process.stdout.write(line + '\n');
|
|
71
|
+
};
|
|
72
|
+
const print_json = (data) => {
|
|
73
|
+
print(JSON.stringify(data, null, 2));
|
|
74
|
+
};
|
|
75
|
+
// The rerun hint for the next page, on stderr (the logger's): shown to a
|
|
76
|
+
// person, invisible to a pipe.
|
|
77
|
+
const more_hint = (page) => page.has_more
|
|
78
|
+
? `… more results: rerun with --starting-after ${page.next_cursor} (or --all)`
|
|
79
|
+
: undefined;
|
|
80
|
+
// Left-padded fixed-width table lines. Cells are stringified as-is; column
|
|
81
|
+
// width = max(header, cells).
|
|
82
|
+
const table_lines = (headers, rows) => {
|
|
83
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map(r => (r[i] ?? '').length)));
|
|
84
|
+
const render = (cells) => cells
|
|
85
|
+
.map((c, i) => (i === cells.length - 1 ? c : (c ?? '').padEnd(widths[i])))
|
|
86
|
+
.join(' ')
|
|
87
|
+
.trimEnd();
|
|
88
|
+
return [render(headers), ...rows.map(render)];
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export { DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, as_list, fetch_page, from_paginator, json_option, list_options, more_hint, print, print_json, table_lines };
|
package/dist/log.js
CHANGED
|
@@ -6,6 +6,10 @@ import { buildLog } from './lib/log_buffer.js';
|
|
|
6
6
|
// transport: the worker's MessagePort can stay referenced after the last log
|
|
7
7
|
// (thread-stream race) and intermittently keep the CLI from exiting.
|
|
8
8
|
//
|
|
9
|
+
// Messages go to STDERR. stdout is for data only — a listing, a JSON
|
|
10
|
+
// document, logs being streamed — so `faable … --json | jq` and the MCP
|
|
11
|
+
// server reading this CLI never get a progress line mixed into it.
|
|
12
|
+
//
|
|
9
13
|
// Tee: terminal stream (colorized) + a plain-text copy into the build-log
|
|
10
14
|
// buffer so CLI messages land in the logs attached to the deployment.
|
|
11
15
|
// What the terminal shows: just the message. pino's envelope
|
|
@@ -35,7 +39,14 @@ const toPlainText = prettyFactory({ colorize: false, sync: true });
|
|
|
35
39
|
// NB: the two-arg form matters — pino(multistream) alone would treat the
|
|
36
40
|
// multistream object as the options bag and log raw JSON to stdout.
|
|
37
41
|
const log = pino({}, pino.multistream([
|
|
38
|
-
{
|
|
42
|
+
{
|
|
43
|
+
stream: pretty({
|
|
44
|
+
colorize: process.stderr.isTTY ?? false,
|
|
45
|
+
sync: true,
|
|
46
|
+
destination: 2,
|
|
47
|
+
...TERMINAL_FORMAT,
|
|
48
|
+
}),
|
|
49
|
+
},
|
|
39
50
|
{
|
|
40
51
|
stream: {
|
|
41
52
|
write(line) {
|
package/package.json
CHANGED