@jinshuju/cli 0.1.1

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.
@@ -0,0 +1,64 @@
1
+ export type ConfigKey = 'access_token' | 'api_key' | 'api_secret' | 'host' | 'auth_host' | 'client_id';
2
+ export type ConfigSource = 'cli' | 'env' | 'file' | 'missing';
3
+ export type OAuthConfig = {
4
+ type: 'oauth';
5
+ auth_host: string;
6
+ client_id: string;
7
+ access_token: string;
8
+ refresh_token?: string;
9
+ expires_at?: string;
10
+ scope?: string;
11
+ };
12
+ export type LoadedConfig = {
13
+ /**
14
+ * A personal or account access token, sent as a bearer. Goldendata accepts it
15
+ * on API v1 alongside the API key pair and an OAuth session.
16
+ */
17
+ accessToken?: string;
18
+ apiKey?: string;
19
+ apiSecret?: string;
20
+ host: string;
21
+ authHost: string;
22
+ clientId?: string;
23
+ auth?: OAuthConfig;
24
+ configPath: string;
25
+ sources: {
26
+ accessToken: ConfigSource;
27
+ apiKey: ConfigSource;
28
+ apiSecret: ConfigSource;
29
+ host: ConfigSource;
30
+ authHost: ConfigSource;
31
+ clientId: ConfigSource;
32
+ auth: ConfigSource;
33
+ };
34
+ };
35
+ export type LoadConfigOptions = {
36
+ configPath?: string;
37
+ env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
38
+ cli?: {
39
+ accessToken?: string;
40
+ apiKey?: string;
41
+ apiSecret?: string;
42
+ host?: string;
43
+ authHost?: string;
44
+ clientId?: string;
45
+ };
46
+ };
47
+ export declare const defaultConfigPath: string;
48
+ export declare const defaultHost = "https://jinshuju.net";
49
+ export declare const defaultAuthHost = "https://account.jinshuju.net";
50
+ export declare const defaultOAuthClientId = "jinshuju_cli_public";
51
+ export declare const defaultScopes = "public forms read_entries write_entries form_setting read_contacts users";
52
+ export type RawConfig = Partial<Record<ConfigKey, string>> & {
53
+ auth?: OAuthConfig;
54
+ };
55
+ export declare function loadConfig(options?: LoadConfigOptions): LoadedConfig;
56
+ export declare function setConfigValue(configPath: string, key: ConfigKey, value: string): void;
57
+ export declare function unsetConfigValue(configPath: string, key: ConfigKey): void;
58
+ export declare function saveOAuthConfig(configPath: string, auth: OAuthConfig): void;
59
+ export declare function clearOAuthConfig(configPath: string): void;
60
+ export declare function getConfig(configPath: string): RawConfig;
61
+ export declare function maskSecret(value: string | undefined): string | undefined;
62
+ /** Every key the config file holds, in the order help should list them. */
63
+ export declare const CONFIG_KEYS: readonly ConfigKey[];
64
+ export declare function assertConfigKey(value: string): asserts value is ConfigKey;
package/dist/config.js ADDED
@@ -0,0 +1,99 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ export const defaultConfigPath = join(homedir(), '.jinshuju', 'config.json');
5
+ export const defaultHost = 'https://jinshuju.net';
6
+ export const defaultAuthHost = 'https://account.jinshuju.net';
7
+ export const defaultOAuthClientId = 'jinshuju_cli_public';
8
+ export const defaultScopes = 'public forms read_entries write_entries form_setting read_contacts users';
9
+ function readConfigFile(configPath) {
10
+ if (!existsSync(configPath))
11
+ return {};
12
+ return JSON.parse(readFileSync(configPath, 'utf8'));
13
+ }
14
+ function writeConfigFile(configPath, config) {
15
+ mkdirSync(dirname(configPath), { recursive: true });
16
+ writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
17
+ }
18
+ function pickValue(cliValue, envValue, fileValue, fallback) {
19
+ if (cliValue)
20
+ return { value: cliValue, source: 'cli' };
21
+ if (envValue)
22
+ return { value: envValue, source: 'env' };
23
+ if (fileValue)
24
+ return { value: fileValue, source: 'file' };
25
+ if (fallback)
26
+ return { value: fallback, source: 'missing' };
27
+ return { source: 'missing' };
28
+ }
29
+ export function loadConfig(options = {}) {
30
+ const configPath = options.configPath ?? defaultConfigPath;
31
+ const env = options.env ?? process.env;
32
+ const file = readConfigFile(configPath);
33
+ const accessToken = pickValue(options.cli?.accessToken, env.JINSHUJU_ACCESS_TOKEN, file.access_token);
34
+ const apiKey = pickValue(options.cli?.apiKey, env.JINSHUJU_API_KEY, file.api_key);
35
+ const apiSecret = pickValue(options.cli?.apiSecret, env.JINSHUJU_API_SECRET, file.api_secret);
36
+ const host = pickValue(options.cli?.host, env.JINSHUJU_HOST, file.host, defaultHost);
37
+ const authHost = pickValue(options.cli?.authHost, env.JINSHUJU_AUTH_HOST, file.auth_host ?? file.auth?.auth_host, defaultAuthHost);
38
+ const defaultClientId = authHost.value === defaultAuthHost ? defaultOAuthClientId : undefined;
39
+ const clientId = pickValue(options.cli?.clientId, env.JINSHUJU_OAUTH_CLIENT_ID, file.client_id ?? file.auth?.client_id, defaultClientId);
40
+ const auth = file.auth?.type === 'oauth' ? file.auth : undefined;
41
+ return {
42
+ accessToken: accessToken.value,
43
+ apiKey: apiKey.value,
44
+ apiSecret: apiSecret.value,
45
+ host: host.value,
46
+ authHost: authHost.value,
47
+ clientId: clientId.value,
48
+ auth,
49
+ configPath,
50
+ sources: {
51
+ accessToken: accessToken.source,
52
+ apiKey: apiKey.source,
53
+ apiSecret: apiSecret.source,
54
+ host: host.source,
55
+ authHost: authHost.source,
56
+ clientId: clientId.source,
57
+ auth: auth ? 'file' : 'missing'
58
+ }
59
+ };
60
+ }
61
+ export function setConfigValue(configPath, key, value) {
62
+ const config = readConfigFile(configPath);
63
+ config[key] = value;
64
+ writeConfigFile(configPath, config);
65
+ }
66
+ export function unsetConfigValue(configPath, key) {
67
+ const config = readConfigFile(configPath);
68
+ delete config[key];
69
+ writeConfigFile(configPath, config);
70
+ }
71
+ export function saveOAuthConfig(configPath, auth) {
72
+ const config = readConfigFile(configPath);
73
+ config.auth = auth;
74
+ config.auth_host = auth.auth_host;
75
+ config.client_id = auth.client_id;
76
+ writeConfigFile(configPath, config);
77
+ }
78
+ export function clearOAuthConfig(configPath) {
79
+ const config = readConfigFile(configPath);
80
+ delete config.auth;
81
+ writeConfigFile(configPath, config);
82
+ }
83
+ export function getConfig(configPath) {
84
+ return readConfigFile(configPath);
85
+ }
86
+ export function maskSecret(value) {
87
+ if (!value)
88
+ return undefined;
89
+ if (value.length <= 8)
90
+ return '••••';
91
+ return `${value.slice(0, 4)}…${value.slice(-4)}`;
92
+ }
93
+ /** Every key the config file holds, in the order help should list them. */
94
+ export const CONFIG_KEYS = ['access_token', 'api_key', 'api_secret', 'host', 'auth_host', 'client_id'];
95
+ export function assertConfigKey(value) {
96
+ if (!CONFIG_KEYS.includes(value)) {
97
+ throw new Error(`Config key must be one of ${CONFIG_KEYS.join(', ')}`);
98
+ }
99
+ }
package/dist/help.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { type Command } from './commands.js';
2
+ /**
3
+ * Help is rendered from the command table, so a command cannot be reachable
4
+ * without appearing here, and a flag cannot be accepted without being
5
+ * described. The root help lists one line per resource, then the global
6
+ * options.
7
+ */
8
+ export declare function rootHelp(commands?: readonly Command[]): string;
9
+ /** Every verb of one resource, for `jinshuju form --help`. */
10
+ export declare function resourceHelp(resource: string, commands?: readonly Command[]): string;
11
+ export declare function commandHelp(command: Command): string;
12
+ /** What the caller typed that matched nothing, and the nearest things that do. */
13
+ export declare function unknownCommandHelp(words: readonly string[], commands?: readonly Command[]): string;
14
+ /** Help for whatever the words point at: a command, a resource, or the root. */
15
+ export declare function helpFor(words: readonly string[], commands?: readonly Command[]): string;
package/dist/help.js ADDED
@@ -0,0 +1,98 @@
1
+ import { COMMANDS, RESOURCES, findCommand } from './commands.js';
2
+ import { GLOBAL_OPTIONS, optionKey } from './options.js';
3
+ /**
4
+ * Help is rendered from the command table, so a command cannot be reachable
5
+ * without appearing here, and a flag cannot be accepted without being
6
+ * described. The root help lists one line per resource, then the global
7
+ * options.
8
+ */
9
+ export function rootHelp(commands = COMMANDS) {
10
+ const present = new Set(commands.map((command) => command.path[0]));
11
+ const resources = RESOURCES.filter((resource) => present.has(resource.name) || resource.name === 'auth' || resource.name === 'config');
12
+ const width = Math.max(...resources.map((resource) => resource.name.length)) + 4;
13
+ return [
14
+ 'Usage: jinshuju <resource> <verb> [args] [flags]',
15
+ '',
16
+ 'Commands:',
17
+ ...resources.map((resource) => ` ${resource.name.padEnd(width)}${resource.summary}`),
18
+ '',
19
+ 'Global Options:',
20
+ ...GLOBAL_OPTIONS.map((option) => ` ${flagLabel(option).padEnd(width + 12)}${option.description}`),
21
+ '',
22
+ 'Run `jinshuju <resource> --help` to see its verbs.',
23
+ ''
24
+ ].join('\n');
25
+ }
26
+ /** Every verb of one resource, for `jinshuju form --help`. */
27
+ export function resourceHelp(resource, commands = COMMANDS) {
28
+ const owned = commands.filter((command) => command.path[0] === resource);
29
+ if (owned.length === 0)
30
+ return rootHelp(commands);
31
+ const width = Math.max(...owned.map((command) => command.path.join(' ').length)) + 4;
32
+ return [
33
+ `Usage: jinshuju ${resource} <verb> [args] [flags]`,
34
+ '',
35
+ 'Commands:',
36
+ ...owned.map((command) => ` ${command.path.join(' ').padEnd(width)}${command.summary}`),
37
+ '',
38
+ `Run \`jinshuju ${resource} <verb> --help\` for one of them.`,
39
+ ''
40
+ ].join('\n');
41
+ }
42
+ export function commandHelp(command) {
43
+ const usage = [
44
+ 'Usage: jinshuju',
45
+ ...command.path,
46
+ ...(command.args ?? []).map((arg) => {
47
+ const name = arg.required ? `<${arg.name}>` : `[${arg.name}]`;
48
+ return arg.variadic ? `${name}...` : name;
49
+ }),
50
+ '[flags]'
51
+ ].join(' ');
52
+ const parts = [usage, '', command.description ?? command.summary];
53
+ if (command.args?.length) {
54
+ const width = Math.max(...command.args.map((arg) => arg.name.length)) + 4;
55
+ parts.push('', 'Arguments:', ...command.args.map((arg) => ` ${arg.name.padEnd(width)}${arg.description}`));
56
+ }
57
+ const options = [...(command.options ?? []), ...GLOBAL_OPTIONS];
58
+ const width = Math.max(...options.map((option) => flagLabel(option).length)) + 4;
59
+ parts.push('', 'Flags:', ...options.map((option) => ` ${flagLabel(option).padEnd(width)}${option.description}`));
60
+ if (command.payload?.length)
61
+ parts.push('', 'Payload:', ...command.payload.map((line) => ` ${line}`));
62
+ if (command.examples?.length)
63
+ parts.push('', 'Examples:', ...command.examples.map((example) => ` ${example}`));
64
+ return `${parts.join('\n')}\n`;
65
+ }
66
+ /** What the caller typed that matched nothing, and the nearest things that do. */
67
+ export function unknownCommandHelp(words, commands = COMMANDS) {
68
+ const typed = words.join(' ');
69
+ const resource = words[0];
70
+ const siblings = commands.filter((command) => command.path[0] === resource).slice(0, 10);
71
+ return [
72
+ `Unknown command: jinshuju ${typed}`,
73
+ ...(siblings.length > 0
74
+ ? ['', 'Did you mean:', ...siblings.map((command) => ` jinshuju ${command.path.join(' ')}`)]
75
+ : []),
76
+ '',
77
+ 'Run `jinshuju --help` for the full list.',
78
+ ''
79
+ ].join('\n');
80
+ }
81
+ /** Help for whatever the words point at: a command, a resource, or the root. */
82
+ export function helpFor(words, commands = COMMANDS) {
83
+ if (words.length === 0)
84
+ return rootHelp(commands);
85
+ const command = findCommand(words, commands);
86
+ if (command)
87
+ return commandHelp(command);
88
+ const resource = words[0];
89
+ if (commands.some((candidate) => candidate.path[0] === resource))
90
+ return resourceHelp(resource, commands);
91
+ return rootHelp(commands);
92
+ }
93
+ function flagLabel(option) {
94
+ const flags = option.short ? `${option.short}, ${option.name}` : option.name;
95
+ if (option.type === 'boolean')
96
+ return flags;
97
+ return `${flags} ${option.placeholder ?? `<${optionKey(option)}>`}`;
98
+ }
package/dist/http.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { type LoadedConfig } from './config.js';
2
+ export type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'DELETE';
3
+ export type HttpRequest = {
4
+ method: HttpMethod | string;
5
+ path: string;
6
+ body?: unknown;
7
+ /**
8
+ * A multipart body, for the three endpoints that take a file. fetch sets its
9
+ * own Content-Type here, boundary included, so the JSON one must not be sent.
10
+ */
11
+ form?: FormData;
12
+ };
13
+ export interface HttpClient {
14
+ request<T>(request: HttpRequest): Promise<T>;
15
+ }
16
+ export declare class JinshujuHttpClient implements HttpClient {
17
+ private config;
18
+ private readonly baseUrl;
19
+ constructor(config?: LoadedConfig, baseUrl?: string);
20
+ request<T>(request: HttpRequest): Promise<T>;
21
+ private requestWithAuth;
22
+ /**
23
+ * An explicitly configured credential outranks a stored session: someone who
24
+ * set a token in the environment meant this request to use it, and finding a
25
+ * login from last week used instead would be a surprise with no signal.
26
+ * `auth status` says which one is in play and where it came from.
27
+ */
28
+ private authorizationHeader;
29
+ }
package/dist/http.js ADDED
@@ -0,0 +1,127 @@
1
+ import { loadConfig } from './config.js';
2
+ import { refreshOAuthToken, shouldRefresh } from './auth.js';
3
+ /** How much of an unexpected body is worth putting in front of a reader. */
4
+ const MAX_SNIPPET = 200;
5
+ function parseBody(text) {
6
+ if (!text)
7
+ return { json: true, value: undefined };
8
+ try {
9
+ return { json: true, value: JSON.parse(text) };
10
+ }
11
+ catch {
12
+ return { json: false };
13
+ }
14
+ }
15
+ function snippet(text) {
16
+ const flat = text.replace(/\s+/g, ' ').trim();
17
+ return flat.length <= MAX_SNIPPET ? flat : `${flat.slice(0, MAX_SNIPPET)}…`;
18
+ }
19
+ /**
20
+ * What to say about a request the server refused.
21
+ *
22
+ * A server that stated a reason has it repeated verbatim, which is the whole of
23
+ * the message and reads as it always did. Everything else falls back through
24
+ * what the body does carry rather than to the HTTP status text: answering
25
+ * "Unprocessable Entity" to a batch whose response named the offending row and
26
+ * field is throwing away the only useful part of the answer.
27
+ */
28
+ function describeFailure(response, text, parsed) {
29
+ const status = `${response.status} ${response.statusText}`.trim();
30
+ if (!parsed.json) {
31
+ return text.trim() ? `${status}, and the body is not JSON: ${snippet(text)}` : status;
32
+ }
33
+ if (parsed.value === undefined || parsed.value === null)
34
+ return status;
35
+ const body = parsed.value;
36
+ const stated = body.message ?? body.error_description;
37
+ if (typeof stated === 'string' && stated.trim())
38
+ return stated;
39
+ return rowErrors(body) ?? `${status}: ${snippet(JSON.stringify(parsed.value))}`;
40
+ }
41
+ /**
42
+ * A batch write answers per row: `{errors: [{index, reason}]}`. The index is the
43
+ * caller's own array position, and is left as the server gave it so it lines up
44
+ * with both the payload that was sent and what `--output json` shows.
45
+ */
46
+ function rowErrors(body) {
47
+ const errors = body.errors;
48
+ if (!Array.isArray(errors) || errors.length === 0)
49
+ return undefined;
50
+ const lines = errors.map((item) => {
51
+ if (typeof item === 'string')
52
+ return item;
53
+ if (item === null || typeof item !== 'object')
54
+ return String(item);
55
+ const { index, reason, message } = item;
56
+ const said = [reason, message].find((value) => typeof value === 'string' && value.trim());
57
+ const text = said ?? JSON.stringify(item);
58
+ return typeof index === 'number' ? `row ${index}: ${text}` : text;
59
+ });
60
+ return `${lines.length} row${lines.length === 1 ? '' : 's'} rejected — ${lines.join('; ')}`;
61
+ }
62
+ export class JinshujuHttpClient {
63
+ config;
64
+ baseUrl;
65
+ constructor(config = loadConfig(), baseUrl = config.host) {
66
+ this.config = config;
67
+ this.baseUrl = baseUrl;
68
+ }
69
+ async request(request) {
70
+ return this.requestWithAuth(request, true);
71
+ }
72
+ async requestWithAuth(request, allowRefresh) {
73
+ const headers = {
74
+ Authorization: await this.authorizationHeader(),
75
+ Accept: 'application/json'
76
+ };
77
+ if (!request.form)
78
+ headers['Content-Type'] = 'application/json';
79
+ const response = await fetch(`${this.baseUrl}${request.path}`, {
80
+ method: request.method,
81
+ headers,
82
+ body: request.form ?? (request.body === undefined ? undefined : JSON.stringify(request.body))
83
+ });
84
+ // Only an OAuth session can be refreshed; an access token that stopped
85
+ // working has to be replaced by whoever issued it.
86
+ if (response.status === 401 && allowRefresh && !this.config.accessToken && this.config.auth?.refresh_token) {
87
+ this.config.auth = await refreshOAuthToken(this.config);
88
+ return this.requestWithAuth(request, false);
89
+ }
90
+ const text = await response.text();
91
+ const parsed = parseBody(text);
92
+ // Parsing is not allowed to decide whether the request failed. A 500 answers
93
+ // with an HTML error page and a gateway with its own, and parsing first
94
+ // turned both into "Unexpected token '<'", throwing away the status that was
95
+ // the only thing that said what had happened.
96
+ if (!response.ok) {
97
+ const error = new Error(describeFailure(response, text, parsed));
98
+ Object.assign(error, { status: response.status, body: parsed.json ? parsed.value : text });
99
+ throw error;
100
+ }
101
+ if (!parsed.json) {
102
+ throw new Error(`${response.status} ${response.statusText}, but the body is not JSON: ${snippet(text)}`);
103
+ }
104
+ return parsed.value;
105
+ }
106
+ /**
107
+ * An explicitly configured credential outranks a stored session: someone who
108
+ * set a token in the environment meant this request to use it, and finding a
109
+ * login from last week used instead would be a surprise with no signal.
110
+ * `auth status` says which one is in play and where it came from.
111
+ */
112
+ async authorizationHeader() {
113
+ if (this.config.accessToken)
114
+ return `Bearer ${this.config.accessToken}`;
115
+ if (this.config.apiKey && this.config.apiSecret) {
116
+ const credentials = Buffer.from(`${this.config.apiKey}:${this.config.apiSecret}`).toString('base64');
117
+ return `Basic ${credentials}`;
118
+ }
119
+ if (this.config.auth?.access_token) {
120
+ if (shouldRefresh(this.config.auth) && this.config.auth.refresh_token) {
121
+ this.config.auth = await refreshOAuthToken(this.config);
122
+ }
123
+ return `Bearer ${this.config.auth.access_token}`;
124
+ }
125
+ throw new Error('Missing authentication. Run `jinshuju auth login`, or configure JINSHUJU_ACCESS_TOKEN, or JINSHUJU_API_KEY with JINSHUJU_API_SECRET.');
126
+ }
127
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The shared option grammar, written once here because every resource uses it:
3
+ * container, filter, sort, pagination, JSON input and output format.
4
+ */
5
+ export type OptionType = 'string' | 'integer' | 'boolean' | 'json' | 'list';
6
+ export interface OptionSpec {
7
+ /** Long flag with dashes, e.g. `--filter`. */
8
+ readonly name: string;
9
+ readonly short?: string;
10
+ readonly type: OptionType;
11
+ /** May be given more than once; values collect into a list. */
12
+ readonly repeatable?: boolean;
13
+ /** Placeholder shown in help, e.g. `<field> <op> [value]`. */
14
+ readonly placeholder?: string;
15
+ readonly choices?: readonly string[];
16
+ readonly description: string;
17
+ }
18
+ export declare const OUTPUT_FORMATS: readonly ["text", "json"];
19
+ export type OutputFormat = (typeof OUTPUT_FORMATS)[number];
20
+ /** Accepted by every command. */
21
+ export declare const GLOBAL_OPTIONS: readonly OptionSpec[];
22
+ /**
23
+ * What the local commands — auth and config, the ones that never reach the API —
24
+ * accept on top of the global options. They live here so the command table can
25
+ * describe them: help is rendered from that table, and a flag nobody can find
26
+ * in it is a flag nobody knows about.
27
+ */
28
+ export declare const LOCAL_OPTIONS: readonly OptionSpec[];
29
+ /** The data container a command acts on. Mutually exclusive; both map to form_token. */
30
+ export declare const CONTAINER_OPTIONS: readonly OptionSpec[];
31
+ /**
32
+ * The same container, repeatable, for the reads that answer about several at
33
+ * once. Still one kind per call: `--form` and `--table` stay mutually exclusive.
34
+ */
35
+ export declare const CONTAINER_LIST_OPTIONS: readonly OptionSpec[];
36
+ export declare const FILTER_OPTION: OptionSpec;
37
+ export declare const FILTERS_OPTION: OptionSpec;
38
+ export declare const SORT_OPTION: OptionSpec;
39
+ /** Asks for a smaller page; a listing's default is also its cap. */
40
+ export declare const LIMIT_OPTION: OptionSpec;
41
+ export declare const PAGINATION_OPTIONS: readonly OptionSpec[];
42
+ export declare const JSON_OPTION: OptionSpec;
43
+ export declare const MINE_OPTION: OptionSpec;
44
+ export declare class UsageError extends Error {
45
+ }
46
+ /** `--api-key` reads back as `api_key`. */
47
+ export declare function optionKey(spec: OptionSpec): string;
48
+ export interface FilterCondition {
49
+ readonly field: string;
50
+ readonly operator: string;
51
+ readonly value?: unknown;
52
+ }
53
+ /**
54
+ * `field_3 gte 80` into `{field, operator, value}`.
55
+ *
56
+ * Values stay strings. The server converts a condition value by the field's own
57
+ * type — a number field runs it through to_f — so a phone number keeps its
58
+ * digits instead of being guessed into a number here. Only the operators whose
59
+ * value has a shape get one built: a pair, a list, or a relative window.
60
+ */
61
+ export declare function parseFilter(input: string): FilterCondition;
62
+ export interface SortRule {
63
+ readonly field: string;
64
+ readonly order: 'asc' | 'desc';
65
+ }
66
+ /** `created_at:desc`; the order defaults to asc, as a bare field reads. */
67
+ export declare function parseSort(input: string): SortRule;
68
+ export interface Metric {
69
+ readonly func: string;
70
+ readonly field: string;
71
+ }
72
+ export interface Dimension {
73
+ readonly field: string;
74
+ readonly bucket?: string;
75
+ }
76
+ export declare const TIME_BUCKETS: readonly ["day", "week", "month"];
77
+ /**
78
+ * `avg:field_3`. Which functions a field takes is the field's own answer — read
79
+ * `analytics.agg_funcs` off `form get --include-analytics` — so the function is
80
+ * passed through rather than checked against a list kept here, the same way an
81
+ * operator is.
82
+ */
83
+ export declare function parseMetric(input: string): Metric;
84
+ /** `field_7`, or `created_at:month` for a date. */
85
+ export declare function parseDimension(input: string): Dimension;
86
+ /** Inline JSON, `@path`, or `-` for stdin. */
87
+ export declare function readJsonInput(raw: string, stdin: () => string): unknown;
88
+ export interface Container {
89
+ readonly token: string;
90
+ readonly kind: 'form' | 'table';
91
+ }
92
+ /**
93
+ * Both `--form` and `--table` address the same API parameter, so exactly one has
94
+ * to be given: a command that guessed would read the wrong object silently.
95
+ */
96
+ export declare function resolveContainer(options: Record<string, unknown>): Container;
97
+ export interface Containers {
98
+ readonly tokens: readonly string[];
99
+ readonly kind: 'form' | 'table';
100
+ }
101
+ /** The repeatable form of the above, for a read that answers about several. */
102
+ export declare function resolveContainers(options: Record<string, unknown>, max: number): Containers;