@formo/cli 0.2.0 → 1.0.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.
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analytics = void 0;
4
+ exports.buildAnalyticsParams = buildAnalyticsParams;
5
+ exports.runAnalytics = runAnalytics;
6
+ const incur_1 = require("incur");
7
+ const client_1 = require("../lib/client");
8
+ exports.analytics = incur_1.Cli.create('analytics', {
9
+ description: 'Pre-built analytics query commands — KPIs, funnels, retention, revenue, and top-N breakdowns',
10
+ });
11
+ // The pre-built analytics pipes exposed at GET /v0/<pipe>. Each requires the
12
+ // query:read scope. Common params (date_from, date_to, filters) are shared;
13
+ // pipe-specific params (e.g. funnel `steps`, kpis `group_by`, `limit`) are
14
+ // passed through the generic --params JSON object.
15
+ const PIPES = [
16
+ { name: 'kpis', description: 'Traffic KPIs: visitors, pageviews, bounce rate, session duration' },
17
+ { name: 'event_timeseries', description: 'Event counts over time' },
18
+ { name: 'funnel', description: 'Conversion funnel across ordered steps. --params: steps (JSON array of {type,event,name,filters?}), window_seconds, funnel_type, breakdown' },
19
+ { name: 'flow', description: 'User path/flow analysis. --params: start_step / end_step (JSON {type,event,...}), global_filters, window_seconds, max_steps' },
20
+ { name: 'frequency', description: 'Engagement frequency distribution' },
21
+ { name: 'lifecycle', description: 'User lifecycle stages (new, returning, power, resurrected, churned)' },
22
+ { name: 'retention', description: 'Retention cohort analysis (params: id_type, event_type, event_name, min_users)' },
23
+ { name: 'revenue_overview', description: 'Revenue overview with optional breakdown (params: group_by, rank_by)' },
24
+ { name: 'revenue_by_metric', description: 'Revenue ranked by a metric column (params: metric_column, limit, offset)' },
25
+ { name: 'revenue_timeseries', description: 'Revenue over time (params: address)' },
26
+ { name: 'volume_by_metric', description: 'Trading volume ranked by a metric column (params: metric_column, limit, offset)' },
27
+ { name: 'top_chains', description: 'Top chains by activity (params: limit, offset)' },
28
+ { name: 'top_events', description: 'Top events by count (params: limit, offset, type)' },
29
+ { name: 'top_locations', description: 'Top locations (params: limit, offset)' },
30
+ { name: 'top_pages', description: 'Top pages by traffic (params: limit, offset, mode)' },
31
+ { name: 'top_sources', description: 'Top acquisition sources (params: metric_column, limit, offset)' },
32
+ { name: 'top_wallets', description: 'Top wallets by activity (params: limit, offset)' },
33
+ ];
34
+ // Keys --params is not allowed to set: they have dedicated, validated flags
35
+ // (--date-from/--date-to/--filters). Rejecting them prevents --params from
36
+ // silently overriding validated input or pushing an invalid `filters` value
37
+ // (e.g. a non-JSON string) over the wire. Both casings of the date keys are
38
+ // rejected so a stray camelCase key can't slip through unvalidated.
39
+ const RESERVED_PARAM_KEYS = new Set([
40
+ 'date_from',
41
+ 'date_to',
42
+ 'dateFrom',
43
+ 'dateTo',
44
+ 'filters',
45
+ ]);
46
+ /**
47
+ * Build the query-string params for an analytics pipe request.
48
+ *
49
+ * - `dateFrom`/`dateTo` map to the API's snake_case `date_from`/`date_to`.
50
+ * All pipes, including `funnel` and `flow`, use snake_case.
51
+ * - `filters` is a JSON array of `{ field, op, value }` objects, re-serialized
52
+ * as a JSON string (the pipe expects a JSON-encoded array in the query).
53
+ * - `params` is a JSON object of any pipe-specific params (e.g. funnel
54
+ * `steps`, kpis `group_by`, `limit`). Object/array values are JSON-encoded
55
+ * (pipes like funnel expect `steps` as a JSON-encoded string); primitives
56
+ * pass through unchanged. Reserved keys (the date/filters flags) are
57
+ * rejected, and the validated flags below always take precedence.
58
+ *
59
+ * Exported for unit testing.
60
+ */
61
+ function buildAnalyticsParams(options) {
62
+ const out = {};
63
+ // --params first, so the validated flags below override it.
64
+ if (options.params) {
65
+ let parsed;
66
+ try {
67
+ parsed = JSON.parse(options.params);
68
+ }
69
+ catch {
70
+ throw new Error('--params must be a valid JSON object');
71
+ }
72
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
73
+ throw new Error('--params must be a valid JSON object');
74
+ }
75
+ for (const [key, value] of Object.entries(parsed)) {
76
+ if (RESERVED_PARAM_KEYS.has(key)) {
77
+ throw new Error(`--params may not set "${key}" — use the --date-from/--date-to/--filters flags instead`);
78
+ }
79
+ if (value === null || value === undefined)
80
+ continue;
81
+ if (typeof value === 'object') {
82
+ out[key] = JSON.stringify(value);
83
+ }
84
+ else {
85
+ out[key] = value;
86
+ }
87
+ }
88
+ }
89
+ if (options.dateFrom)
90
+ out.date_from = options.dateFrom;
91
+ if (options.dateTo)
92
+ out.date_to = options.dateTo;
93
+ if (options.filters) {
94
+ let parsed;
95
+ try {
96
+ parsed = JSON.parse(options.filters);
97
+ }
98
+ catch {
99
+ throw new Error('--filters must be a valid JSON array of {field,op,value} objects');
100
+ }
101
+ if (!Array.isArray(parsed)) {
102
+ throw new Error('--filters must be a valid JSON array of {field,op,value} objects');
103
+ }
104
+ out.filters = JSON.stringify(parsed);
105
+ }
106
+ return out;
107
+ }
108
+ function runAnalytics(pipe, options) {
109
+ (0, client_1.requireApiKey)();
110
+ const client = (0, client_1.createClient)();
111
+ return client.get(`/v0/${pipe}`, { params: buildAnalyticsParams(options) });
112
+ }
113
+ const sharedOptions = incur_1.z.object({
114
+ dateFrom: incur_1.z
115
+ .string()
116
+ .optional()
117
+ .describe('Inclusive start date YYYY-MM-DD (default: 7 days before --date-to)'),
118
+ dateTo: incur_1.z
119
+ .string()
120
+ .optional()
121
+ .describe('Inclusive end date YYYY-MM-DD (default: today)'),
122
+ filters: incur_1.z
123
+ .string()
124
+ .optional()
125
+ .describe('JSON array of filter conditions: [{"field","op","value"}]. ' +
126
+ 'Use op "in"/"notIn" with a pipe-delimited value (e.g. "chrome|firefox").'),
127
+ params: incur_1.z
128
+ .string()
129
+ .optional()
130
+ .describe('JSON object of pipe-specific params merged into the query, e.g. ' +
131
+ '{"limit":10,"group_by":"device"} or funnel ' +
132
+ '{"steps":[{"type":"event","event":"page","name":"page::0","filters":[]}]}. ' +
133
+ 'May not set date_from/date_to/filters; use the dedicated --date-from/--date-to/--filters flags.'),
134
+ });
135
+ for (const pipe of PIPES) {
136
+ exports.analytics.command(pipe.name, {
137
+ description: pipe.description,
138
+ options: sharedOptions,
139
+ examples: [
140
+ {
141
+ description: `Get ${pipe.name} for the last 7 days (default range)`,
142
+ },
143
+ {
144
+ options: { dateFrom: '2026-04-01', dateTo: '2026-04-30' },
145
+ description: `Get ${pipe.name} for April 2026`,
146
+ },
147
+ ],
148
+ hint: 'Requires query:read scope on your API key. Pass pipe-specific params via --params.',
149
+ run({ options }) {
150
+ return runAnalytics(pipe.name, options);
151
+ },
152
+ });
153
+ }
@@ -1,15 +1,24 @@
1
1
  import { Cli } from 'incur';
2
2
  export declare const boards: Cli.Cli<{}, undefined, undefined>;
3
- export declare function listBoardsRun(): Promise<import("axios").AxiosResponse<any, any, {}>>;
3
+ export interface PaginationOptions {
4
+ page?: number;
5
+ size?: number;
6
+ }
7
+ export declare function listBoardsRun(options?: PaginationOptions): Promise<import("axios").AxiosResponse<any, any, {}>>;
4
8
  export declare function getBoardRun(boardId: string): Promise<import("axios").AxiosResponse<any, any, {}>>;
5
9
  export interface CreateBoardOptions {
6
- name: string;
10
+ title?: string;
11
+ name?: string;
7
12
  description?: string;
13
+ isPublic?: boolean;
8
14
  }
15
+ export declare function buildBoardBody(options: CreateBoardOptions | UpdateBoardOptions): Record<string, unknown>;
9
16
  export declare function createBoardRun(options: CreateBoardOptions): Promise<import("axios").AxiosResponse<any, any, {}>>;
10
17
  export interface UpdateBoardOptions {
18
+ title?: string;
11
19
  name?: string;
12
20
  description?: string;
21
+ isPublic?: boolean;
13
22
  }
14
23
  export declare function updateBoardRun(boardId: string, options: UpdateBoardOptions): Promise<import("axios").AxiosResponse<any, any, {}>>;
15
24
  export declare function deleteBoardRun(boardId: string): Promise<import("axios").AxiosResponse<any, any, {}>>;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.boards = void 0;
4
4
  exports.listBoardsRun = listBoardsRun;
5
5
  exports.getBoardRun = getBoardRun;
6
+ exports.buildBoardBody = buildBoardBody;
6
7
  exports.createBoardRun = createBoardRun;
7
8
  exports.updateBoardRun = updateBoardRun;
8
9
  exports.deleteBoardRun = deleteBoardRun;
@@ -11,18 +12,30 @@ const client_1 = require("../lib/client");
11
12
  exports.boards = incur_1.Cli.create('boards', {
12
13
  description: 'Dashboard board commands — create, list, update, and delete boards',
13
14
  });
15
+ function buildPaginationParams(options = {}) {
16
+ const params = {};
17
+ if (options.page !== undefined)
18
+ params.page = options.page;
19
+ if (options.size !== undefined)
20
+ params.size = options.size;
21
+ return params;
22
+ }
14
23
  // ── List boards ──
15
- function listBoardsRun() {
24
+ function listBoardsRun(options = {}) {
16
25
  (0, client_1.requireApiKey)();
17
26
  const client = (0, client_1.createClient)();
18
- return client.get('/v0/boards/');
27
+ return client.get('/v0/boards/', { params: buildPaginationParams(options) });
19
28
  }
20
29
  exports.boards.command('list', {
21
30
  description: 'List all boards for the project',
31
+ options: incur_1.z.object({
32
+ page: incur_1.z.coerce.number().optional().describe('Page number (1-indexed, default 1)'),
33
+ size: incur_1.z.coerce.number().optional().describe('Page size (default 100, max 200)'),
34
+ }),
22
35
  examples: [{ description: 'List all dashboard boards' }],
23
36
  hint: 'Requires boards:read scope on your API key.',
24
- run() {
25
- return listBoardsRun();
37
+ run({ options }) {
38
+ return listBoardsRun(options);
26
39
  },
27
40
  });
28
41
  // ── Get a single board ──
@@ -44,28 +57,46 @@ exports.boards.command('get', {
44
57
  return getBoardRun(args.boardId);
45
58
  },
46
59
  });
60
+ function buildBoardBody(options) {
61
+ const title = options.title ?? options.name;
62
+ const body = {};
63
+ if (title !== undefined) {
64
+ if (!title)
65
+ throw new Error('--title must not be empty');
66
+ body.title = title;
67
+ }
68
+ if (options.description !== undefined) {
69
+ body.description = options.description;
70
+ }
71
+ if (options.isPublic !== undefined) {
72
+ body.isPublic = options.isPublic;
73
+ }
74
+ return body;
75
+ }
47
76
  function createBoardRun(options) {
48
77
  (0, client_1.requireApiKey)();
49
78
  const client = (0, client_1.createClient)();
50
- const body = { name: options.name };
51
- if (options.description) {
52
- body.description = options.description;
79
+ const body = buildBoardBody(options);
80
+ if (!body.title) {
81
+ throw new Error('Provide --title for the board name');
53
82
  }
54
83
  return client.post('/v0/boards/', body);
55
84
  }
56
85
  exports.boards.command('create', {
57
86
  description: 'Create a new dashboard board',
58
87
  options: incur_1.z.object({
59
- name: incur_1.z.string().describe('Board name'),
88
+ title: incur_1.z.string().optional().describe('Board title'),
89
+ name: incur_1.z.string().optional().describe('Deprecated alias for --title').meta({ deprecated: true }),
60
90
  description: incur_1.z.string().optional().describe('Board description'),
91
+ isPublic: incur_1.z.boolean().optional().describe('Whether the board is publicly viewable'),
61
92
  }),
62
93
  examples: [
63
94
  {
64
- options: { name: 'KPI Dashboard' },
95
+ options: { title: 'KPI Dashboard' },
65
96
  description: 'Create a board',
66
97
  },
67
98
  {
68
- options: { name: 'Revenue Metrics', description: 'Weekly revenue tracking' },
99
+ options: { title: 'Revenue Metrics', description: 'Weekly revenue tracking' },
69
100
  description: 'Create a board with description',
70
101
  },
71
102
  ],
@@ -77,11 +108,10 @@ exports.boards.command('create', {
77
108
  function updateBoardRun(boardId, options) {
78
109
  (0, client_1.requireApiKey)();
79
110
  const client = (0, client_1.createClient)();
80
- const body = {};
81
- if (options.name !== undefined)
82
- body.name = options.name;
83
- if (options.description !== undefined)
84
- body.description = options.description;
111
+ const body = buildBoardBody(options);
112
+ if (Object.keys(body).length === 0) {
113
+ throw new Error('Provide at least one of --title, --description, or --is-public');
114
+ }
85
115
  return client.patch(`/v0/boards/${encodeURIComponent(boardId)}`, body);
86
116
  }
87
117
  exports.boards.command('update', {
@@ -90,13 +120,15 @@ exports.boards.command('update', {
90
120
  boardId: incur_1.z.string().describe('Board ID to update'),
91
121
  }),
92
122
  options: incur_1.z.object({
93
- name: incur_1.z.string().optional().describe('New board name'),
123
+ title: incur_1.z.string().optional().describe('New board title'),
124
+ name: incur_1.z.string().optional().describe('Deprecated alias for --title').meta({ deprecated: true }),
94
125
  description: incur_1.z.string().optional().describe('New board description'),
126
+ isPublic: incur_1.z.boolean().optional().describe('Whether the board is publicly viewable'),
95
127
  }),
96
128
  examples: [
97
129
  {
98
130
  args: { boardId: 'board_abc123' },
99
- options: { name: 'Renamed Board' },
131
+ options: { title: 'Renamed Board' },
100
132
  description: 'Rename a board',
101
133
  },
102
134
  ],
@@ -1,7 +1,34 @@
1
1
  import { Cli } from 'incur';
2
2
  export declare const charts: Cli.Cli<{}, undefined, undefined>;
3
- export declare function listChartsRun(boardId: string): Promise<import("axios").AxiosResponse<any, any, {}>>;
3
+ export interface PaginationOptions {
4
+ page?: number;
5
+ size?: number;
6
+ }
7
+ export interface ChartBodyOptions {
8
+ body?: string;
9
+ query?: string;
10
+ chartType?: string;
11
+ title?: string;
12
+ description?: string;
13
+ xAxis?: string;
14
+ yAxis?: string;
15
+ groupBy?: string;
16
+ steps?: string;
17
+ settings?: string;
18
+ }
19
+ export declare function buildChartBody(options: ChartBodyOptions): Record<string, unknown>;
20
+ export declare function listChartsRun(boardId: string, options?: PaginationOptions): Promise<import("axios").AxiosResponse<any, any, {}>>;
21
+ export declare function listChartSummariesRun(boardId: string, options?: PaginationOptions): Promise<import("axios").AxiosResponse<any, any, {}>>;
4
22
  export declare function getChartRun(boardId: string, chartId: string): Promise<import("axios").AxiosResponse<any, any, {}>>;
5
- export declare function createChartRun(boardId: string, body: string): Promise<import("axios").AxiosResponse<any, any, {}>>;
6
- export declare function updateChartRun(boardId: string, chartId: string, body: string): Promise<import("axios").AxiosResponse<any, any, {}>>;
23
+ export interface QueryChartOptions {
24
+ dateFrom: string;
25
+ dateTo: string;
26
+ }
27
+ export declare function queryChartRun(boardId: string, chartId: string, options: QueryChartOptions): Promise<import("axios").AxiosResponse<any, any, {}>>;
28
+ export declare function createChartRun(boardId: string, input: string | ChartBodyOptions): Promise<import("axios").AxiosResponse<any, any, {}>>;
29
+ export declare function updateChartRun(boardId: string, chartId: string, input: string | ChartBodyOptions): Promise<import("axios").AxiosResponse<any, any, {}>>;
30
+ export declare function moveChartRun(boardId: string, chartId: string, targetBoardId: string): Promise<import("axios").AxiosResponse<any, any, {}>>;
31
+ export declare function normalizeDuplicateChartResponse(result: unknown): unknown;
32
+ export declare function duplicateChartRun(boardId: string, chartId: string): Promise<unknown>;
33
+ export declare function reorderChartsRun(boardId: string, chartIds: string): Promise<import("axios").AxiosResponse<any, any, {}>>;
7
34
  export declare function deleteChartRun(boardId: string, chartId: string): Promise<import("axios").AxiosResponse<any, any, {}>>;