@formo/cli 1.1.0 → 1.2.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/README.md +92 -31
- package/dist/commands/alerts.d.ts +14 -22
- package/dist/commands/alerts.js +48 -49
- package/dist/commands/analytics.d.ts +1 -1
- package/dist/commands/analytics.js +51 -11
- package/dist/commands/boards.d.ts +7 -9
- package/dist/commands/boards.js +4 -14
- package/dist/commands/charts.d.ts +11 -13
- package/dist/commands/charts.js +12 -16
- package/dist/commands/contracts.d.ts +9 -11
- package/dist/commands/contracts.js +7 -17
- package/dist/commands/events.d.ts +1 -1
- package/dist/commands/events.js +6 -5
- package/dist/commands/import.d.ts +1 -1
- package/dist/commands/import.js +3 -0
- package/dist/commands/profiles.d.ts +13 -13
- package/dist/commands/profiles.js +189 -49
- package/dist/commands/query.d.ts +1 -1
- package/dist/commands/segments.d.ts +7 -9
- package/dist/commands/segments.js +45 -25
- package/dist/index.js +39 -26
- package/dist/lib/client.d.ts +18 -3
- package/dist/lib/client.js +11 -2
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.js +32 -18
- package/dist/lib/filters.d.ts +6 -0
- package/dist/lib/filters.js +45 -0
- package/dist/lib/pagination.d.ts +14 -0
- package/dist/lib/pagination.js +31 -0
- package/dist/lib/ui.d.ts +6 -5
- package/dist/lib/ui.js +10 -12
- package/package.json +8 -12
package/dist/lib/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AxiosError } from 'axios';
|
|
1
|
+
import { AxiosError, type AxiosInstance, type AxiosRequestConfig } from 'axios';
|
|
2
2
|
export declare const DEFAULT_API_BASE_URL = "https://api.formo.so";
|
|
3
3
|
export declare const DEFAULT_EVENTS_BASE_URL = "https://events.formo.so";
|
|
4
4
|
export declare function getApiBaseUrl(): string;
|
|
@@ -32,7 +32,22 @@ export interface ClientOptions {
|
|
|
32
32
|
baseURL?: string;
|
|
33
33
|
apiKey?: string;
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
/**
|
|
36
|
+
* The response interceptor below unwraps every response to its body
|
|
37
|
+
* (`res.data`), so the raw AxiosInstance types would lie — they promise
|
|
38
|
+
* `AxiosResponse<T>` while the runtime value is the body itself. This
|
|
39
|
+
* interface states what actually comes back.
|
|
40
|
+
*/
|
|
41
|
+
export interface FormoClient {
|
|
42
|
+
get(url: string, config?: AxiosRequestConfig): Promise<unknown>;
|
|
43
|
+
post(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<unknown>;
|
|
44
|
+
put(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<unknown>;
|
|
45
|
+
patch(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<unknown>;
|
|
46
|
+
delete(url: string, config?: AxiosRequestConfig): Promise<unknown>;
|
|
47
|
+
request(config: AxiosRequestConfig): Promise<unknown>;
|
|
48
|
+
defaults: AxiosInstance['defaults'];
|
|
49
|
+
}
|
|
50
|
+
declare function createClient(options?: ClientOptions): FormoClient;
|
|
51
|
+
export declare function createEventsClient(writeKey: string): FormoClient;
|
|
37
52
|
export declare function requireApiKey(): void;
|
|
38
53
|
export { createClient };
|
package/dist/lib/client.js
CHANGED
|
@@ -38,7 +38,9 @@ function parseApiError(error) {
|
|
|
38
38
|
parts.push(`Param: ${apiError.param}`);
|
|
39
39
|
if (apiError?.details && Object.keys(apiError.details).length > 0) {
|
|
40
40
|
const details = Object.entries(apiError.details)
|
|
41
|
-
.map(([key, value]) => `${key}: ${
|
|
41
|
+
.map(([key, value]) => `${key}: ${typeof value === 'object' && value !== null
|
|
42
|
+
? JSON.stringify(value)
|
|
43
|
+
: String(value)}`)
|
|
42
44
|
.join('; ');
|
|
43
45
|
parts.push(`Details: ${details}`);
|
|
44
46
|
}
|
|
@@ -57,17 +59,24 @@ function parseApiError(error) {
|
|
|
57
59
|
function createClient(options = {}) {
|
|
58
60
|
const apiKey = options.apiKey ?? (0, config_1.getApiKey)();
|
|
59
61
|
const baseURL = options.baseURL ?? getApiBaseUrl();
|
|
62
|
+
// Fail here, not with a server-side 401: every authenticated command needs
|
|
63
|
+
// a key, and this guard catches call sites that forget requireApiKey().
|
|
64
|
+
if (!apiKey) {
|
|
65
|
+
throw new Error('No API key configured. Run `formo login <apiKey>` or set FORMO_API_KEY env var.');
|
|
66
|
+
}
|
|
60
67
|
const instance = axios_1.default.create({
|
|
61
68
|
baseURL,
|
|
62
69
|
timeout: 30000,
|
|
63
70
|
headers: {
|
|
64
71
|
'Content-Type': 'application/json',
|
|
65
|
-
|
|
72
|
+
Authorization: `Bearer ${apiKey}`,
|
|
66
73
|
},
|
|
67
74
|
});
|
|
68
75
|
instance.interceptors.response.use((res) => res.data, (error) => {
|
|
69
76
|
throw parseApiError(error);
|
|
70
77
|
});
|
|
78
|
+
// The interceptor changes what the promise resolves to; the cast makes the
|
|
79
|
+
// public type match the runtime behavior (see FormoClient above).
|
|
71
80
|
return instance;
|
|
72
81
|
}
|
|
73
82
|
function createEventsClient(writeKey) {
|
package/dist/lib/config.d.ts
CHANGED
package/dist/lib/config.js
CHANGED
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getConfigFile = getConfigFile;
|
|
6
7
|
exports.readConfig = readConfig;
|
|
7
8
|
exports.saveConfig = saveConfig;
|
|
8
9
|
exports.clearConfig = clearConfig;
|
|
@@ -10,12 +11,24 @@ exports.getApiKey = getApiKey;
|
|
|
10
11
|
const fs_1 = __importDefault(require("fs"));
|
|
11
12
|
const os_1 = __importDefault(require("os"));
|
|
12
13
|
const path_1 = __importDefault(require("path"));
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
// FORMO_CONFIG_DIR lets tests (and users) redirect config away from the real
|
|
15
|
+
// ~/.config/formo — resolved lazily so a test can set it after import.
|
|
16
|
+
function configDir() {
|
|
17
|
+
return (process.env.FORMO_CONFIG_DIR ?? path_1.default.join(os_1.default.homedir(), '.config', 'formo'));
|
|
18
|
+
}
|
|
19
|
+
function getConfigFile() {
|
|
20
|
+
return path_1.default.join(configDir(), 'config.json');
|
|
21
|
+
}
|
|
15
22
|
function readConfig() {
|
|
16
23
|
try {
|
|
17
|
-
const raw = fs_1.default.readFileSync(
|
|
18
|
-
|
|
24
|
+
const raw = fs_1.default.readFileSync(getConfigFile(), 'utf-8');
|
|
25
|
+
const parsed = JSON.parse(raw);
|
|
26
|
+
// JSON.parse happily returns null/"abc"/[1] — treat anything that isn't a
|
|
27
|
+
// plain object as an empty config instead of crashing later callers.
|
|
28
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
return parsed;
|
|
19
32
|
}
|
|
20
33
|
catch {
|
|
21
34
|
return {};
|
|
@@ -29,26 +42,27 @@ function saveConfig(updates) {
|
|
|
29
42
|
// tool, another app under ~/.config) keeps its old, possibly
|
|
30
43
|
// group/world-readable perms — leaking the plaintext API key on a
|
|
31
44
|
// multi-user host. chmod unconditionally so 0o700/0o600 always holds.
|
|
32
|
-
fs_1.default.mkdirSync(
|
|
33
|
-
fs_1.default.chmodSync(
|
|
34
|
-
fs_1.default.writeFileSync(
|
|
45
|
+
fs_1.default.mkdirSync(configDir(), { recursive: true, mode: 0o700 });
|
|
46
|
+
fs_1.default.chmodSync(configDir(), 0o700);
|
|
47
|
+
fs_1.default.writeFileSync(getConfigFile(), JSON.stringify(merged, null, 2), {
|
|
35
48
|
mode: 0o600,
|
|
36
49
|
});
|
|
37
|
-
fs_1.default.chmodSync(
|
|
50
|
+
fs_1.default.chmodSync(getConfigFile(), 0o600);
|
|
38
51
|
}
|
|
39
52
|
function clearConfig() {
|
|
40
53
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
fs_1.default.chmodSync(CONFIG_FILE, 0o600);
|
|
48
|
-
}
|
|
54
|
+
fs_1.default.writeFileSync(getConfigFile(), JSON.stringify({}, null, 2), {
|
|
55
|
+
mode: 0o600,
|
|
56
|
+
});
|
|
57
|
+
// Same create-only-mode caveat as saveConfig: enforce 0o600 on the
|
|
58
|
+
// already-existing file so the cleared config can't be left readable.
|
|
59
|
+
fs_1.default.chmodSync(getConfigFile(), 0o600);
|
|
49
60
|
}
|
|
50
|
-
catch {
|
|
51
|
-
//
|
|
61
|
+
catch (err) {
|
|
62
|
+
// Nothing to clear is fine; a real write failure (EACCES, EROFS) must
|
|
63
|
+
// surface so `formo logout` can't claim success while the key remains.
|
|
64
|
+
if (err.code !== 'ENOENT')
|
|
65
|
+
throw err;
|
|
52
66
|
}
|
|
53
67
|
}
|
|
54
68
|
function getApiKey() {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const CANONICAL_FILTER_OPERATORS: readonly ["eq", "neq", "gt", "lt", "gte", "lte", "in", "nin", "startsWith", "endsWith", "contains", "notEmpty", "isEmpty"];
|
|
2
|
+
export declare function isCanonicalFilterOperator(op: unknown): op is string;
|
|
3
|
+
export declare function isValuelessFilterOperator(op: unknown): boolean;
|
|
4
|
+
export declare function isCanonicalFilterValue(value: unknown): boolean;
|
|
5
|
+
export declare function isEmptyMembershipArray(value: unknown): boolean;
|
|
6
|
+
export declare function hasTinybirdMembershipDelimiter(value: unknown): boolean;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CANONICAL_FILTER_OPERATORS = void 0;
|
|
4
|
+
exports.isCanonicalFilterOperator = isCanonicalFilterOperator;
|
|
5
|
+
exports.isValuelessFilterOperator = isValuelessFilterOperator;
|
|
6
|
+
exports.isCanonicalFilterValue = isCanonicalFilterValue;
|
|
7
|
+
exports.isEmptyMembershipArray = isEmptyMembershipArray;
|
|
8
|
+
exports.hasTinybirdMembershipDelimiter = hasTinybirdMembershipDelimiter;
|
|
9
|
+
exports.CANONICAL_FILTER_OPERATORS = [
|
|
10
|
+
'eq',
|
|
11
|
+
'neq',
|
|
12
|
+
'gt',
|
|
13
|
+
'lt',
|
|
14
|
+
'gte',
|
|
15
|
+
'lte',
|
|
16
|
+
'in',
|
|
17
|
+
'nin',
|
|
18
|
+
'startsWith',
|
|
19
|
+
'endsWith',
|
|
20
|
+
'contains',
|
|
21
|
+
'notEmpty',
|
|
22
|
+
'isEmpty',
|
|
23
|
+
];
|
|
24
|
+
const CANONICAL_FILTER_OPERATOR_SET = new Set(exports.CANONICAL_FILTER_OPERATORS);
|
|
25
|
+
function isCanonicalFilterOperator(op) {
|
|
26
|
+
return typeof op === 'string' && CANONICAL_FILTER_OPERATOR_SET.has(op);
|
|
27
|
+
}
|
|
28
|
+
function isValuelessFilterOperator(op) {
|
|
29
|
+
return op === 'notEmpty' || op === 'isEmpty';
|
|
30
|
+
}
|
|
31
|
+
function isCanonicalFilterValue(value) {
|
|
32
|
+
return (typeof value === 'string' ||
|
|
33
|
+
typeof value === 'number' ||
|
|
34
|
+
typeof value === 'boolean' ||
|
|
35
|
+
(Array.isArray(value) &&
|
|
36
|
+
value.length > 0 &&
|
|
37
|
+
value.every((item) => typeof item === 'string' || typeof item === 'number')));
|
|
38
|
+
}
|
|
39
|
+
function isEmptyMembershipArray(value) {
|
|
40
|
+
return Array.isArray(value) && value.length === 0;
|
|
41
|
+
}
|
|
42
|
+
function hasTinybirdMembershipDelimiter(value) {
|
|
43
|
+
return (Array.isArray(value) &&
|
|
44
|
+
value.some((item) => typeof item === 'string' && item.includes('|')));
|
|
45
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from 'incur';
|
|
2
|
+
export interface PaginationOptions {
|
|
3
|
+
page?: number;
|
|
4
|
+
size?: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Shared zod fragments for paginated list commands.
|
|
8
|
+
* Spread into a command's options: `z.object({ ...paginationOptionsSchema })`.
|
|
9
|
+
*/
|
|
10
|
+
export declare const paginationOptionsSchema: {
|
|
11
|
+
page: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
|
|
12
|
+
size: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
|
|
13
|
+
};
|
|
14
|
+
export declare function buildPaginationParams(options?: PaginationOptions): Record<string, number>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.paginationOptionsSchema = void 0;
|
|
4
|
+
exports.buildPaginationParams = buildPaginationParams;
|
|
5
|
+
const incur_1 = require("incur");
|
|
6
|
+
/**
|
|
7
|
+
* Shared zod fragments for paginated list commands.
|
|
8
|
+
* Spread into a command's options: `z.object({ ...paginationOptionsSchema })`.
|
|
9
|
+
*/
|
|
10
|
+
exports.paginationOptionsSchema = {
|
|
11
|
+
page: incur_1.z.coerce
|
|
12
|
+
.number()
|
|
13
|
+
.int()
|
|
14
|
+
.positive()
|
|
15
|
+
.optional()
|
|
16
|
+
.describe('Page number (1-indexed, default 1)'),
|
|
17
|
+
size: incur_1.z.coerce
|
|
18
|
+
.number()
|
|
19
|
+
.int()
|
|
20
|
+
.positive()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe('Page size (default 100, max 200)'),
|
|
23
|
+
};
|
|
24
|
+
function buildPaginationParams(options = {}) {
|
|
25
|
+
const params = {};
|
|
26
|
+
if (options.page !== undefined)
|
|
27
|
+
params.page = options.page;
|
|
28
|
+
if (options.size !== undefined)
|
|
29
|
+
params.size = options.size;
|
|
30
|
+
return params;
|
|
31
|
+
}
|
package/dist/lib/ui.d.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Terminal UI utilities for the Formo CLI.
|
|
3
3
|
*
|
|
4
4
|
* Uses raw ANSI escape codes (no dependencies) for coloring and styling.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* All decorated output is written to stderr (stdout is reserved for command
|
|
6
|
+
* results), so color is keyed to stderr being a TTY. Disabled whenever the
|
|
7
|
+
* NO_COLOR environment variable is present (any value, per the spec).
|
|
7
8
|
*/
|
|
8
9
|
export declare const color: {
|
|
9
10
|
green: (t: string) => string;
|
|
@@ -20,12 +21,12 @@ export declare const color: {
|
|
|
20
21
|
};
|
|
21
22
|
/**
|
|
22
23
|
* Returns the Formo ASCII art banner in green.
|
|
23
|
-
* Only shown when
|
|
24
|
+
* Only shown when stderr is a TTY.
|
|
24
25
|
*/
|
|
25
26
|
export declare function banner(): string;
|
|
26
27
|
export declare function success(message: string): string;
|
|
27
28
|
export declare function error(message: string): string;
|
|
28
29
|
export declare function warn(message: string): string;
|
|
29
30
|
export declare function info(message: string): string;
|
|
30
|
-
|
|
31
|
-
export declare function
|
|
31
|
+
/** Mask an API key for display, keeping just enough to identify it. */
|
|
32
|
+
export declare function maskKey(key: string): string;
|
package/dist/lib/ui.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* Terminal UI utilities for the Formo CLI.
|
|
4
4
|
*
|
|
5
5
|
* Uses raw ANSI escape codes (no dependencies) for coloring and styling.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* All decorated output is written to stderr (stdout is reserved for command
|
|
7
|
+
* results), so color is keyed to stderr being a TTY. Disabled whenever the
|
|
8
|
+
* NO_COLOR environment variable is present (any value, per the spec).
|
|
8
9
|
*/
|
|
9
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
11
|
exports.color = void 0;
|
|
@@ -13,10 +14,9 @@ exports.success = success;
|
|
|
13
14
|
exports.error = error;
|
|
14
15
|
exports.warn = warn;
|
|
15
16
|
exports.info = info;
|
|
16
|
-
exports.
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const noColor = !!process.env.NO_COLOR;
|
|
17
|
+
exports.maskKey = maskKey;
|
|
18
|
+
const isTTY = process.stderr.isTTY === true;
|
|
19
|
+
const noColor = process.env.NO_COLOR !== undefined;
|
|
20
20
|
/** Whether color output is enabled */
|
|
21
21
|
const colorEnabled = isTTY && !noColor;
|
|
22
22
|
// ── ANSI helpers ──
|
|
@@ -47,7 +47,7 @@ const LOGO_LINES = [
|
|
|
47
47
|
];
|
|
48
48
|
/**
|
|
49
49
|
* Returns the Formo ASCII art banner in green.
|
|
50
|
-
* Only shown when
|
|
50
|
+
* Only shown when stderr is a TTY.
|
|
51
51
|
*/
|
|
52
52
|
function banner() {
|
|
53
53
|
if (!isTTY)
|
|
@@ -71,9 +71,7 @@ function info(message) {
|
|
|
71
71
|
return `${exports.color.cyan('ℹ')} ${message}`;
|
|
72
72
|
}
|
|
73
73
|
// ── Formatting helpers ──
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
function heading(text) {
|
|
78
|
-
return exports.color.boldGreen(text);
|
|
74
|
+
/** Mask an API key for display, keeping just enough to identify it. */
|
|
75
|
+
function maskKey(key) {
|
|
76
|
+
return key.length > 12 ? key.slice(0, 8) + '…' + key.slice(-4) : '***';
|
|
79
77
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@formo/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"packageManager": "pnpm@11.1.2",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=22.12"
|
|
7
|
+
},
|
|
5
8
|
"description": "Formo API CLI — query profiles and analytics data",
|
|
6
9
|
"license": "MIT",
|
|
7
10
|
"repository": {
|
|
@@ -22,19 +25,18 @@
|
|
|
22
25
|
],
|
|
23
26
|
"scripts": {
|
|
24
27
|
"build": "tsc",
|
|
25
|
-
"
|
|
28
|
+
"prepublishOnly": "pnpm build",
|
|
29
|
+
"dev": "tsx src/index.ts",
|
|
26
30
|
"start": "node dist/index.js",
|
|
27
31
|
"lint": "eslint src/",
|
|
28
32
|
"lint:fix": "eslint src/ --fix",
|
|
33
|
+
"typecheck": "tsc -p tsconfig.test.json --noEmit",
|
|
29
34
|
"test": "mocha",
|
|
30
35
|
"test:watch": "mocha --watch"
|
|
31
36
|
},
|
|
32
37
|
"dependencies": {
|
|
33
38
|
"axios": "^1.18.0",
|
|
34
|
-
"incur": "
|
|
35
|
-
},
|
|
36
|
-
"overrides": {
|
|
37
|
-
"serialize-javascript": ">=7.0.5"
|
|
39
|
+
"incur": "0.3.25"
|
|
38
40
|
},
|
|
39
41
|
"devDependencies": {
|
|
40
42
|
"@eslint/js": "^10.0.1",
|
|
@@ -46,14 +48,8 @@
|
|
|
46
48
|
"eslint": "^10.2.0",
|
|
47
49
|
"globals": "^17.4.0",
|
|
48
50
|
"mocha": "^11.7.5",
|
|
49
|
-
"ts-node": "^10.9.2",
|
|
50
51
|
"tsx": "^4.21.0",
|
|
51
52
|
"typescript": "^5.5.4",
|
|
52
53
|
"typescript-eslint": "^8.58.1"
|
|
53
|
-
},
|
|
54
|
-
"pnpm": {
|
|
55
|
-
"overrides": {
|
|
56
|
-
"serialize-javascript": ">=7.0.5"
|
|
57
|
-
}
|
|
58
54
|
}
|
|
59
55
|
}
|