@invinite-org/invinite-data-cli 2.0.0 → 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 (129) hide show
  1. package/README.md +341 -374
  2. package/claude-plugin/.claude-plugin/plugin.json +1 -1
  3. package/claude-plugin/agents/invinite-data.md +59 -28
  4. package/claude-plugin/commands/analyze-earnings-call.md +15 -14
  5. package/claude-plugin/commands/analyze-filing.md +8 -7
  6. package/claude-plugin/commands/compare-companies.md +5 -5
  7. package/claude-plugin/commands/financial-research.md +9 -9
  8. package/claude-plugin/commands/ownership-review.md +6 -6
  9. package/claude-plugin/skills/invinite-data-cli/SKILL.md +466 -570
  10. package/dist/bin/invd.js +11 -2
  11. package/dist/bin/invd.js.map +1 -1
  12. package/dist/src/cli.d.ts +2 -0
  13. package/dist/src/cli.js +48 -29
  14. package/dist/src/cli.js.map +1 -1
  15. package/dist/src/client/api-client.d.ts +24 -24
  16. package/dist/src/client/api-client.js +52 -70
  17. package/dist/src/client/api-client.js.map +1 -1
  18. package/dist/src/client/pagination.d.ts +12 -11
  19. package/dist/src/client/pagination.js +47 -56
  20. package/dist/src/client/pagination.js.map +1 -1
  21. package/dist/src/commands/agent-setup.d.ts +21 -0
  22. package/dist/src/commands/agent-setup.js +112 -0
  23. package/dist/src/commands/agent-setup.js.map +1 -0
  24. package/dist/src/commands/api.d.ts +27 -0
  25. package/dist/src/commands/api.js +180 -0
  26. package/dist/src/commands/api.js.map +1 -0
  27. package/dist/src/commands/auth.d.ts +37 -0
  28. package/dist/src/commands/auth.js +336 -0
  29. package/dist/src/commands/auth.js.map +1 -0
  30. package/dist/src/commands/completion.d.ts +7 -0
  31. package/dist/src/commands/completion.js +115 -0
  32. package/dist/src/commands/completion.js.map +1 -0
  33. package/dist/src/commands/config.d.ts +4 -0
  34. package/dist/src/commands/config.js +85 -48
  35. package/dist/src/commands/config.js.map +1 -1
  36. package/dist/src/commands/plugin.d.ts +8 -0
  37. package/dist/src/commands/plugin.js +8 -10
  38. package/dist/src/commands/plugin.js.map +1 -1
  39. package/dist/src/config/auth.d.ts +17 -1
  40. package/dist/src/config/auth.js +50 -36
  41. package/dist/src/config/auth.js.map +1 -1
  42. package/dist/src/config/config-manager.d.ts +56 -4
  43. package/dist/src/config/config-manager.js +173 -29
  44. package/dist/src/config/config-manager.js.map +1 -1
  45. package/dist/src/config/toml.d.ts +7 -0
  46. package/dist/src/config/toml.js +79 -0
  47. package/dist/src/config/toml.js.map +1 -0
  48. package/dist/src/examples.d.ts +22 -0
  49. package/dist/src/examples.js +56 -0
  50. package/dist/src/examples.js.map +1 -0
  51. package/dist/src/generated/cli-manifest.json +5583 -4916
  52. package/dist/src/hand-written-commands.d.ts +11 -41
  53. package/dist/src/hand-written-commands.js +7 -17
  54. package/dist/src/hand-written-commands.js.map +1 -1
  55. package/dist/src/output/jq.d.ts +15 -0
  56. package/dist/src/output/jq.js +234 -0
  57. package/dist/src/output/jq.js.map +1 -0
  58. package/dist/src/output/options.d.ts +29 -0
  59. package/dist/src/output/options.js +72 -0
  60. package/dist/src/output/options.js.map +1 -0
  61. package/dist/src/output/render.d.ts +54 -0
  62. package/dist/src/output/render.js +160 -0
  63. package/dist/src/output/render.js.map +1 -0
  64. package/dist/src/shared/errors.d.ts +59 -15
  65. package/dist/src/shared/errors.js +126 -38
  66. package/dist/src/shared/errors.js.map +1 -1
  67. package/dist/src/shared/prompt.d.ts +10 -0
  68. package/dist/src/shared/prompt.js +24 -0
  69. package/dist/src/shared/prompt.js.map +1 -0
  70. package/dist/src/shared/spinner.js +2 -1
  71. package/dist/src/shared/spinner.js.map +1 -1
  72. package/dist/src/spec-commands.d.ts +14 -6
  73. package/dist/src/spec-commands.js +454 -174
  74. package/dist/src/spec-commands.js.map +1 -1
  75. package/dist/src/spec-flags.d.ts +11 -6
  76. package/dist/src/spec-flags.js +14 -7
  77. package/dist/src/spec-flags.js.map +1 -1
  78. package/dist/src/spec-manifest.d.ts +416 -116
  79. package/dist/src/spec-manifest.js +44 -13
  80. package/dist/src/spec-manifest.js.map +1 -1
  81. package/dist/src/update-notifier.d.ts +9 -0
  82. package/dist/src/update-notifier.js +63 -0
  83. package/dist/src/update-notifier.js.map +1 -0
  84. package/package.json +2 -3
  85. package/dist/src/client/earnings-live.d.ts +0 -194
  86. package/dist/src/client/earnings-live.js +0 -300
  87. package/dist/src/client/earnings-live.js.map +0 -1
  88. package/dist/src/client/news-live.d.ts +0 -72
  89. package/dist/src/client/news-live.js +0 -120
  90. package/dist/src/client/news-live.js.map +0 -1
  91. package/dist/src/client/pdf.d.ts +0 -15
  92. package/dist/src/client/pdf.js +0 -15
  93. package/dist/src/client/pdf.js.map +0 -1
  94. package/dist/src/client/websocket-client.d.ts +0 -19
  95. package/dist/src/client/websocket-client.js +0 -174
  96. package/dist/src/client/websocket-client.js.map +0 -1
  97. package/dist/src/commands/earnings.d.ts +0 -16
  98. package/dist/src/commands/earnings.js +0 -464
  99. package/dist/src/commands/earnings.js.map +0 -1
  100. package/dist/src/commands/login.d.ts +0 -15
  101. package/dist/src/commands/login.js +0 -249
  102. package/dist/src/commands/login.js.map +0 -1
  103. package/dist/src/commands/news.d.ts +0 -2
  104. package/dist/src/commands/news.js +0 -69
  105. package/dist/src/commands/news.js.map +0 -1
  106. package/dist/src/commands/sec.d.ts +0 -47
  107. package/dist/src/commands/sec.js +0 -191
  108. package/dist/src/commands/sec.js.map +0 -1
  109. package/dist/src/commands/ws.d.ts +0 -33
  110. package/dist/src/commands/ws.js +0 -116
  111. package/dist/src/commands/ws.js.map +0 -1
  112. package/dist/src/formatters/json-formatter.d.ts +0 -1
  113. package/dist/src/formatters/json-formatter.js +0 -4
  114. package/dist/src/formatters/json-formatter.js.map +0 -1
  115. package/dist/src/formatters/table-formatter.d.ts +0 -4
  116. package/dist/src/formatters/table-formatter.js +0 -73
  117. package/dist/src/formatters/table-formatter.js.map +0 -1
  118. package/dist/src/formatters/time.d.ts +0 -2
  119. package/dist/src/formatters/time.js +0 -14
  120. package/dist/src/formatters/time.js.map +0 -1
  121. package/dist/src/shared/output.d.ts +0 -3
  122. package/dist/src/shared/output.js +0 -13
  123. package/dist/src/shared/output.js.map +0 -1
  124. package/dist/src/shared/readiness.d.ts +0 -4
  125. package/dist/src/shared/readiness.js +0 -60
  126. package/dist/src/shared/readiness.js.map +0 -1
  127. package/dist/src/shared/types.d.ts +0 -11
  128. package/dist/src/shared/types.js +0 -2
  129. package/dist/src/shared/types.js.map +0 -1
@@ -1,231 +1,511 @@
1
1
  import { readFile } from 'node:fs/promises';
2
+ import chalk from 'chalk';
2
3
  import { Option } from 'commander';
3
- import { z } from 'zod';
4
- import { apiGet, apiRequest } from './client/api-client.js';
5
- import { fetchAllCompanyPages, fetchAllTopPages } from './client/pagination.js';
6
- import { flattenRows, pick } from './formatters/flatten.js';
7
- import { formatJson } from './formatters/json-formatter.js';
8
- import { formatKeyValue, formatTable, printPaginationHint } from './formatters/table-formatter.js';
9
- import { apiErrorHint, withErrorHandling } from './shared/errors.js';
4
+ import { apiCall, buildUrl } from './client/api-client.js';
5
+ import { saveVerified } from './client/download.js';
6
+ import { nextCursorOf, rowsOf, walkPages } from './client/pagination.js';
7
+ import { getSetting } from './config/config-manager.js';
8
+ import { exampleLines } from './examples.js';
9
+ import { pick } from './formatters/flatten.js';
10
+ import { runJq } from './output/jq.js';
11
+ import { addOutputOptions, resolveOutput } from './output/options.js';
12
+ import { bodyRows, csvColumns, csvHeader, csvRow, emit, humanText, renderBody, shapeBody, } from './output/render.js';
13
+ import { PartialResultError, UsageError, withErrorHandling } from './shared/errors.js';
10
14
  import { splitList } from './shared/options.js';
11
- import { output } from './shared/output.js';
12
- import { printReadinessNotes } from './shared/readiness.js';
13
- import { setJsonMode } from './shared/spinner.js';
14
- import { flagName } from './spec-flags.js';
15
- import { exampleValue } from './spec-manifest.js';
15
+ import { confirmOrRefuse, readStdin } from './shared/prompt.js';
16
+ import { GLOBAL_PARAM_FLAGS } from './spec-flags.js';
17
+ import { commandName, exampleValue, } from './spec-manifest.js';
16
18
  /**
17
- * Registers every manifest operation as `invd <group> <action>` (`action: ""` → `invd <group>`): one flag per
18
- * parameter (path parameters included, never positional), values validated before any request, the request through
19
- * `apiGet` / `apiRequest` (keyless where the spec says so), `--all` by the operation's pagination, and the operation's
20
- * view as the human output. A group a hand-written command already registered (`earnings`, whose download commands
21
- * are rostered in `src/hand-written-commands.ts`) gets the generated actions attached; commander refuses a duplicate.
19
+ * Registers the manifest's operations: `invd <group> <action>` (a group's `list` is its default, so
20
+ * `invd filings AAPL` lists), one positional for the natural key, one flag per parameter, the output / paging /
21
+ * metering flags, and `invd financials` as ONE command over its six operations (`--basis` × `--view`).
22
22
  */
23
23
  export function registerSpecCommands(program, manifest) {
24
+ const listEvents = manifest.operations.find(operation => operation.operationId === 'listEvents');
24
25
  for (const group of manifest.groups) {
25
26
  const operations = manifest.operations.filter(operation => operation.group === group.name);
26
- const groupCommand = (program.commands.find(command => command.name() === group.name) ?? program.command(group.name))
27
- .description(group.description);
27
+ const groupCommand = program.command(group.name).description(group.description);
28
+ const multi = operations.filter(operation => operation.action === '');
29
+ if (multi.length > 0) {
30
+ defineSelectorCommand(groupCommand, multi, { listEvents });
31
+ continue;
32
+ }
28
33
  for (const operation of operations) {
29
- const command = operation.action === ''
30
- ? groupCommand
31
- : groupCommand.command(operation.action).description(operation.summary);
32
- defineOperationCommand(command, operation);
34
+ const command = groupCommand
35
+ .command(operation.action, operation.action === 'list' ? { isDefault: true } : {})
36
+ .description(operation.summary);
37
+ defineOperationCommand(command, operation, { listEvents });
33
38
  }
34
39
  }
35
40
  }
36
41
  const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
37
- /** The value placeholder commander shows after a parameter's flag (a boolean takes none). */
38
- const VALUE_PLACEHOLDER = {
39
- boolean: '',
40
- array: ' <list>',
41
- string: ' <value>',
42
- integer: ' <value>',
43
- number: ' <value>',
44
- };
45
- /** Where each pagination shape keeps the next page's URL (`none` has none). */
46
- const NEXT_URL_PATH = {
47
- none: undefined,
48
- top: 'next_url',
49
- perCompany: 'companies.0.next_url',
50
- };
51
42
  const HTTP_METHOD = {
52
- post: 'POST',
53
- put: 'PUT',
54
- patch: 'PATCH',
55
- delete: 'DELETE',
43
+ get: 'GET', post: 'POST', put: 'PUT', patch: 'PATCH', delete: 'DELETE',
44
+ };
45
+ /** Commander's attribute name of a long flag (`filed-at-gte` → `filedAtGte`). */
46
+ const attributeName = (flag) => flag.replace(/-([a-z0-9])/g, (_match, char) => char.toUpperCase());
47
+ /** Parameters the CLI fills when the flag is absent (spec proposal D10: `period` default on the financials). */
48
+ const IMPLIED = {
49
+ period: values => (['1', '2', '3'].includes(String(values.get('fiscal_quarter'))) ? 'quarterly' : 'annual'),
56
50
  };
57
- function paramOption(param) {
58
- const long = `--${flagName(param.name)}`;
59
- const value = VALUE_PLACEHOLDER[param.type];
51
+ const IMPLIED_NOTE = { period: 'quarterly with --fiscal-quarter 1–3, else annual' };
52
+ const impliedFor = (operation) => operation.select ? operation.params.filter(param => param.name in IMPLIED && param.required).map(param => param.name) : [];
53
+ function describeParam(param) {
60
54
  const notes = [];
61
- if (param.type === 'array') {
55
+ if (param.type === 'array')
62
56
  notes.push(param.enum ? `comma-separated: ${param.enum.join(', ')}` : 'comma-separated');
63
- }
64
57
  if (param.default !== undefined)
65
58
  notes.push(`default: ${JSON.stringify(param.default)}`);
66
59
  if (param.example !== undefined)
67
60
  notes.push(`e.g. ${exampleValue(param.example)}`);
68
- const description = [param.description ?? '', notes.length ? `(${notes.join('; ')})` : '']
69
- .filter(Boolean).join(' ');
70
- const option = new Option(`${param.alias ? `-${param.alias}, ` : ''}${long}${value}`, description);
71
- if (param.required)
61
+ return [param.description ?? '', notes.length ? `(${notes.join('; ')})` : ''].filter(Boolean).join(' ');
62
+ }
63
+ /** The options of one parameter: `--flag <value>`, or `--flag` / `--no-flag` for a boolean. */
64
+ function paramOptions(param, required) {
65
+ const short = param.alias ? `-${param.alias}, ` : '';
66
+ if (param.type === 'boolean') {
67
+ return [
68
+ new Option(`${short}--${param.flag}`, `${describeParam(param)} (sends true)`),
69
+ new Option(`--no-${param.flag}`, `Sends ${param.name}=false`),
70
+ ];
71
+ }
72
+ const option = new Option(`${short}--${param.flag} <${param.type === 'array' ? 'list' : 'value'}>`, describeParam(param));
73
+ if (required)
72
74
  option.makeOptionMandatory();
73
75
  if (param.enum && param.type !== 'array')
74
76
  option.choices(param.enum);
75
- return option;
76
- }
77
- const itemSchema = (type, values) => {
78
- if (values && values.length > 0)
79
- return z.string().refine(value => values.includes(value));
80
- if (type === 'integer')
81
- return z.string().regex(/^-?\d+$/);
82
- if (type === 'number')
83
- return z.string().refine(value => value.trim() !== '' && Number.isFinite(Number(value)));
84
- return z.string();
85
- };
86
- /** The zod check of one parameter's raw flag value, yielding what goes on the wire. */
87
- function paramSchema(param) {
88
- if (param.type === 'boolean')
89
- return z.boolean().transform(value => String(value));
90
- if (param.type === 'array') {
91
- return z.string().transform(value => splitList(value)).pipe(z.array(itemSchema(param.items, param.enum)).min(1));
92
- }
93
- if (param.type === 'integer' || param.type === 'number') {
94
- return z.string().pipe(itemSchema(param.type, undefined)).transform(value => String(Number(value)));
77
+ return [option];
78
+ }
79
+ function bodyOptions(field) {
80
+ const short = field.alias ? `-${field.alias}, ` : '';
81
+ const description = `${field.description ?? ''} (body ${field.path.join('.')})`.trim();
82
+ if (field.type === 'boolean') {
83
+ return [new Option(`${short}--${field.flag}`, `${description}: true`), new Option(`--no-${field.flag}`, `${description}: false`)];
95
84
  }
96
- return z.string();
85
+ return [new Option(`${short}--${field.flag} <${field.type === 'array' ? 'list' : 'value'}>`, description)];
97
86
  }
98
87
  function expectation(param) {
99
- if (param.type === 'array') {
100
- const item = param.enum ? `one of ${param.enum.join(', ')}` : param.items === 'string' ? 'a value' : `an ${param.items}`;
101
- return `a comma-separated list, each item ${item}`;
102
- }
88
+ if (param.type === 'array')
89
+ return `a comma-separated list${param.enum ? ` of ${param.enum.join(', ')}` : ''}`;
90
+ if (param.enum)
91
+ return `one of ${param.enum.join(', ')}`;
103
92
  if (param.type === 'integer')
104
93
  return 'an integer';
105
94
  if (param.type === 'number')
106
95
  return 'a number';
107
- if (param.type === 'boolean')
108
- return 'no value';
96
+ if (param.pattern)
97
+ return `a value matching ${param.pattern}`;
109
98
  return 'a value';
110
99
  }
111
- /** Every given flag validated and converted; refuses by flag name, before any request. */
112
- function wireValues(bindings, opts) {
100
+ /** One flag value validated and converted to what goes on the wire; refuses by flag name (exit 2). */
101
+ export function wireValue(param, raw) {
102
+ const fail = () => new UsageError(`--${param.flag} expects ${expectation(param)} (got ${JSON.stringify(raw)})`);
103
+ if (param.type === 'boolean') {
104
+ if (typeof raw !== 'boolean')
105
+ throw fail();
106
+ return String(raw);
107
+ }
108
+ const text = String(raw);
109
+ if (param.type === 'array') {
110
+ const items = splitList(text);
111
+ const itemOk = (item) => (param.enum ? param.enum.includes(item) : true)
112
+ && (param.items === 'integer' ? /^-?\d+$/.test(item) : true)
113
+ && (param.items === 'number' ? Number.isFinite(Number(item)) : true);
114
+ if (items.length === 0 || !items.every(itemOk))
115
+ throw fail();
116
+ return items;
117
+ }
118
+ if (param.type === 'integer' && !/^-?\d+$/.test(text))
119
+ throw fail();
120
+ if (param.type === 'number' && (text.trim() === '' || !Number.isFinite(Number(text))))
121
+ throw fail();
122
+ if (param.enum && !param.enum.includes(text))
123
+ throw fail();
124
+ if (param.pattern && !new RegExp(param.pattern, 'u').test(text))
125
+ throw fail();
126
+ return param.type === 'integer' || param.type === 'number' ? String(Number(text)) : text;
127
+ }
128
+ /** `2026Q3`, `2026 Q3`, `FY2026Q3` → fiscal year and quarter. */
129
+ export function parseFiscalPeriod(text) {
130
+ const match = /^(?:FY)?(\d{4})\s*-?\s*Q([1-4])$/i.exec(text.trim());
131
+ return match?.[1] && match[2] ? { fiscalYear: match[1], fiscalQuarter: match[2] } : undefined;
132
+ }
133
+ const helpExamples = (lines) => `Examples:\n${lines.map(line => ` $ ${line}`).join('\n')}\n`;
134
+ function helpFooter(operations) {
135
+ const [first] = operations;
136
+ if (!first)
137
+ return '';
138
+ const cost = operations.length === 1
139
+ ? `Cost: ${first.credits} credit${first.credits === 1 ? '' : 's'} per response · plan: ${first.tier}`
140
+ : `Cost per response: ${operations.map(operation => `${Object.values(operation.select ?? {}).join('/')} ${operation.credits} (${operation.tier})`).join(', ')}`;
141
+ const fields = first.fields.length ? `\nFields (--json a,b): ${first.fields.join(', ')}` : '';
142
+ return `\n${cost}${fields}\n`;
143
+ }
144
+ function addPositionals(command, operation) {
145
+ const positional = operation.positional;
146
+ if (!positional)
147
+ return;
148
+ const param = operation.params.find(candidate => candidate.name === positional.name);
149
+ const description = param?.description ?? positional.name;
150
+ if (positional.name === 'event_id') {
151
+ command.argument('<event_id>', `${description}; or a company followed by a fiscal period (NVDA 2026Q3)`);
152
+ command.argument('[fiscal_period]', 'With a company in place of the event id: the fiscal period (2026Q3), resolved through GET /v2/events');
153
+ return;
154
+ }
155
+ const name = positional.variadic ? `${positional.name}...` : positional.name;
156
+ const note = positional.variadic === 'comma' ? ' (one or more; sent as a comma list)' : '';
157
+ command.argument(positional.required ? `<${name}>` : `[${name}]`, `${description}${note}`);
158
+ }
159
+ function addParamOptions(command, operation, params) {
160
+ const implied = impliedFor(operation);
161
+ for (const param of params) {
162
+ if (param.name === operation.positional?.name || GLOBAL_PARAM_FLAGS.includes(param.flag))
163
+ continue;
164
+ const options = paramOptions(param, param.required && !implied.includes(param.name));
165
+ if (implied.includes(param.name) && options[0]) {
166
+ options[0].description = `${options[0].description} — absent: ${IMPLIED_NOTE[param.name] ?? 'filled by invd'}`;
167
+ }
168
+ for (const option of options)
169
+ command.addOption(option);
170
+ }
171
+ }
172
+ function addCommandOptions(command, operation) {
173
+ if (operation.body) {
174
+ for (const field of operation.body.fields)
175
+ for (const option of bodyOptions(field))
176
+ command.addOption(option);
177
+ command.option('--body <json>', 'The whole JSON request body (flags above override its fields)');
178
+ command.option('--body-file <path>', 'Read the JSON request body from a file (- = stdin)');
179
+ }
180
+ if (operation.download !== undefined) {
181
+ command.option('--save <file>', 'Save the file this response points to (size and SHA-256 verified)');
182
+ command.option('--force', 'With --save: replace an existing file');
183
+ }
184
+ addOutputOptions(command, { list: operation.list, fieldsParam: operation.params.some(param => param.name === 'fields') });
185
+ }
186
+ function defineOperationCommand(command, operation, context) {
187
+ addPositionals(command, operation);
188
+ addParamOptions(command, operation, operation.params);
189
+ addCommandOptions(command, operation);
190
+ command.addHelpText('before', helpExamples(exampleLines(operation)));
191
+ command.addHelpText('after', helpFooter([operation]));
192
+ command.action(withErrorHandling(async (...args) => {
193
+ const invoked = args[args.length - 1];
194
+ await runOperation(operation, invoked.processedArgs, invoked.opts(), context);
195
+ }));
196
+ }
197
+ /** `invd financials`: one command, the operation picked by the selector flags (`--basis`, `--view`). */
198
+ function defineSelectorCommand(command, operations, context) {
199
+ const keys = [...new Set(operations.flatMap(operation => Object.keys(operation.select ?? {})))];
200
+ const choices = (key) => [...new Set(operations.map(operation => operation.select?.[key] ?? ''))];
201
+ // The first operation of each key's first value is the default (standardized, summary).
202
+ const primary = operations.find(operation => keys.every(key => operation.select?.[key] === defaultChoice(key, choices(key)))) ?? operations[0];
203
+ if (!primary)
204
+ return;
205
+ const params = new Map();
206
+ for (const operation of operations)
207
+ for (const param of operation.params)
208
+ if (!params.has(param.name))
209
+ params.set(param.name, param);
210
+ addPositionals(command, primary);
211
+ for (const key of keys) {
212
+ command.addOption(new Option(`--${key} <${key}>`, `Which ${key}`).choices(choices(key)).default(defaultChoice(key, choices(key))));
213
+ }
214
+ addParamOptions(command, primary, [...params.values()]);
215
+ addCommandOptions(command, primary);
216
+ const [first] = exampleLines(primary, { implied: impliedFor(primary) });
217
+ const detailed = operations.find(operation => operation.select?.view === 'detailed' && operation.select?.basis === primary.select?.basis);
218
+ const [, second] = exampleLines(detailed ?? primary, { implied: impliedFor(primary), extra: detailed ? ['--view detailed'] : [] });
219
+ command.addHelpText('before', helpExamples([first, second]));
220
+ command.addHelpText('after', helpFooter(operations));
221
+ command.action(withErrorHandling(async (...args) => {
222
+ const invoked = args[args.length - 1];
223
+ const opts = invoked.opts();
224
+ const operation = operations.find(candidate => keys.every(key => candidate.select?.[key] === opts[key]));
225
+ if (!operation)
226
+ throw new UsageError(`No operation for ${keys.map(key => `--${key} ${String(opts[key])}`).join(' ')}`);
227
+ const foreign = [...params.values()].filter(param => !operation.params.some(own => own.name === param.name) && opts[attributeName(param.flag)] !== undefined);
228
+ if (foreign.length > 0) {
229
+ const where = operations.filter(candidate => candidate.params.some(own => own.name === foreign[0]?.name))
230
+ .map(candidate => keys.map(key => `--${key} ${candidate.select?.[key]}`).join(' '));
231
+ const bases = [...new Set(where.map(spelling => spelling.split(' --view')[0]))];
232
+ throw new UsageError(`--${foreign[0]?.flag} applies to ${(bases.length === 1 ? bases : where).join(' | ')} only`);
233
+ }
234
+ await runOperation(operation, invoked.processedArgs, opts, context);
235
+ }));
236
+ }
237
+ const defaultChoice = (key, values) => ({ basis: 'standardized', view: 'summary' }[key] ?? values[0] ?? '');
238
+ /** The request this command line describes, validated; nothing is sent. */
239
+ async function planRequest(operation, positionals, opts, context) {
113
240
  const values = new Map();
114
- for (const { param, option } of bindings) {
115
- const raw = opts[option.attributeName()];
241
+ const positional = operation.positional;
242
+ let lookup;
243
+ if (positional) {
244
+ const [first, second] = positionals;
245
+ if (positional.name === 'event_id' && typeof second === 'string' && second !== '') {
246
+ const period = parseFiscalPeriod(second);
247
+ if (!period)
248
+ throw new UsageError(`${second} is not a fiscal period (expected e.g. 2026Q3)`);
249
+ lookup = {
250
+ request: {
251
+ path: '/v2/events',
252
+ query: { company: String(first), fiscal_year: period.fiscalYear, fiscal_quarter: period.fiscalQuarter, limit: '1' },
253
+ },
254
+ company: String(first),
255
+ period: second,
256
+ credits: context.listEvents?.credits ?? 1,
257
+ };
258
+ }
259
+ else if (Array.isArray(first)) {
260
+ if (first.length > 0)
261
+ values.set(positional.name, first.map(String).join(positional.variadic === 'space' ? ' ' : ','));
262
+ }
263
+ else if (first !== undefined) {
264
+ values.set(positional.name, String(first));
265
+ }
266
+ }
267
+ for (const param of operation.params) {
268
+ if (param.name === positional?.name)
269
+ continue;
270
+ const raw = GLOBAL_PARAM_FLAGS.includes(param.flag) ? opts[param.flag] : opts[attributeName(param.flag)];
116
271
  if (raw === undefined)
117
272
  continue;
118
- const parsed = paramSchema(param).safeParse(raw);
119
- if (!parsed.success) {
120
- throw new Error(`--${flagName(param.name)} expects ${expectation(param)} (got ${JSON.stringify(raw)})`);
121
- }
122
- values.set(param.name, parsed.data);
273
+ values.set(param.name, wireValue(param, raw));
274
+ }
275
+ for (const name of impliedFor(operation)) {
276
+ if (values.has(name))
277
+ continue;
278
+ const value = IMPLIED[name]?.(values);
279
+ if (value === undefined)
280
+ continue;
281
+ values.set(name, value);
282
+ console.error(chalk.dim(`--${name} ${value} assumed (pass --${name} to choose)`));
283
+ }
284
+ const query = {};
285
+ const headers = {};
286
+ for (const param of operation.params) {
287
+ const value = values.get(param.name);
288
+ if (param.in === 'query')
289
+ query[param.name] = value;
290
+ if (param.in === 'header' && typeof value === 'string')
291
+ headers[param.name] = value;
123
292
  }
124
- return values;
293
+ const path = operation.path.replace(/\{([^}]+)\}/g, (_match, name) => {
294
+ if (lookup && name === 'event_id')
295
+ return '{event_id}';
296
+ const value = values.get(name);
297
+ if (typeof value !== 'string' || value === '')
298
+ throw new UsageError(`Missing ${name}`);
299
+ return encodeURIComponent(value);
300
+ });
301
+ const body = operation.body ? await requestBody(operation, opts) : undefined;
302
+ return {
303
+ request: {
304
+ path,
305
+ method: HTTP_METHOD[operation.method],
306
+ query,
307
+ ...(Object.keys(headers).length ? { headers } : {}),
308
+ ...(body !== undefined ? { body } : {}),
309
+ auth: operation.auth,
310
+ },
311
+ ...(lookup ? { lookup } : {}),
312
+ };
125
313
  }
126
- async function readStdin() {
127
- const chunks = [];
128
- for await (const chunk of process.stdin)
129
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
130
- return Buffer.concat(chunks).toString('utf8');
314
+ function setPath(target, path, value) {
315
+ const [head, ...rest] = path;
316
+ if (head === undefined)
317
+ return;
318
+ if (rest.length === 0) {
319
+ target[head] = value;
320
+ return;
321
+ }
322
+ const child = isRecord(target[head]) ? target[head] : {};
323
+ target[head] = child;
324
+ setPath(child, rest, value);
131
325
  }
132
- /** `--body <json>` or `--body-file <path|->` → the parsed JSON body; `undefined` when neither was given. */
326
+ /** `--body` / `--body-file` (JSON), then each body flag over it; `undefined` when nothing was given. */
133
327
  async function requestBody(operation, opts) {
134
328
  const inline = typeof opts.body === 'string' ? opts.body : undefined;
135
329
  const file = typeof opts.bodyFile === 'string' ? opts.bodyFile : undefined;
136
330
  if (inline !== undefined && file !== undefined)
137
- throw new Error('Pass --body or --body-file, not both');
138
- if (inline === undefined && file === undefined) {
139
- if (operation.body?.required)
140
- throw new Error('This command needs a JSON body: --body <json> or --body-file <path|->');
331
+ throw new UsageError('Pass --body or --body-file, not both');
332
+ let body;
333
+ if (inline !== undefined || file !== undefined) {
334
+ const [flag, text] = inline !== undefined
335
+ ? ['--body', inline]
336
+ : [`--body-file ${file}`, file === '-' ? await readStdin() : await readFile(file ?? '', 'utf8')];
337
+ try {
338
+ body = JSON.parse(text);
339
+ }
340
+ catch (error) {
341
+ throw new UsageError(`${flag} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
342
+ }
343
+ }
344
+ for (const field of operation.body?.fields ?? []) {
345
+ const raw = opts[attributeName(field.flag)];
346
+ if (raw === undefined)
347
+ continue;
348
+ if (body !== undefined && !isRecord(body))
349
+ throw new UsageError('--body must be a JSON object to combine with field flags');
350
+ body ??= {};
351
+ const value = field.type === 'array' ? splitList(String(raw)) : field.type === 'integer' || field.type === 'number' ? Number(raw) : raw;
352
+ setPath(body, field.path, value);
353
+ }
354
+ if (body === undefined && operation.body?.required) {
355
+ const flags = operation.body.fields.map(field => `--${field.flag}`);
356
+ throw new UsageError(`This command needs a request body: ${flags.length ? `${flags.join(', ')}, or ` : ''}--body / --body-file`);
357
+ }
358
+ return body;
359
+ }
360
+ const describeRequest = (request) => `${request.method ?? 'GET'} ${buildUrl(request.path, request.query).replace('%7Bevent_id%7D', '<event_id>')}`;
361
+ function printPlan(operation, plan, opts, all) {
362
+ const asJson = opts.json !== undefined || opts.jq !== undefined || opts.output === 'json' || opts.output === 'jsonl';
363
+ const steps = [
364
+ ...(plan.lookup ? [{ step: 'resolve', request: describeRequest(plan.lookup.request), operation: 'listEvents', credits: plan.lookup.credits }] : []),
365
+ {
366
+ step: 'request',
367
+ request: describeRequest(plan.request),
368
+ operation: operation.operationId,
369
+ tier: operation.tier,
370
+ credits: operation.credits,
371
+ ...(plan.request.body !== undefined ? { body: plan.request.body } : {}),
372
+ },
373
+ ];
374
+ const perPage = operation.credits;
375
+ const lookupCredits = plan.lookup?.credits ?? 0;
376
+ const estimate = all
377
+ ? `${lookupCredits ? `${lookupCredits} + ` : ''}${perPage} × pages (the page count is known only during the walk)`
378
+ : String(lookupCredits + perPage);
379
+ if (asJson) {
380
+ emit(JSON.stringify({ dry_run: true, steps, all, credits_estimate: estimate }, null, opts.output === 'jsonl' ? undefined : 2));
381
+ return;
382
+ }
383
+ const lines = steps.map(step => `${step.step}: ${step.request}\n ${step.operation}${'tier' in step ? ` (${step.tier})` : ''} · ${step.credits} credit(s) per response${'body' in step ? `\n body: ${JSON.stringify(step.body)}` : ''}`);
384
+ lines.push(`credits: ${estimate}${perPage > 0 ? ' (+1 per started 1,000 rows beyond the first 1,000 of a response)' : ''}`);
385
+ lines.push('dry run: no request was sent');
386
+ emit(lines.join('\n'));
387
+ }
388
+ /** The yellow stderr line of a `meta.coverage` that is not complete (v2 readiness: a 200 with `state`). */
389
+ export function coverageNote(body) {
390
+ const coverage = pick(body, 'meta.coverage');
391
+ if (!isRecord(coverage) || typeof coverage.state !== 'string' || coverage.state === 'complete')
141
392
  return undefined;
393
+ const through = typeof coverage.through === 'string' ? ` through ${coverage.through}` : '';
394
+ const retry = typeof coverage.retry_after_s === 'number' ? `; retry in ${coverage.retry_after_s}s` : '';
395
+ const meaning = coverage.state === 'pending' ? 'not available yet — no rows is not "nothing exists"' : 'the answer is incomplete';
396
+ return `coverage: ${coverage.state}${through} — ${meaning}${retry}`;
397
+ }
398
+ async function resolveLookup(plan) {
399
+ if (!plan.lookup)
400
+ return plan;
401
+ const { body } = await apiCall({ ...plan.lookup.request, silent: true });
402
+ const row = isRecord(body) && Array.isArray(body.data) ? body.data[0] : undefined;
403
+ const eventId = isRecord(row) && typeof row.event_id === 'string' ? row.event_id : undefined;
404
+ if (!eventId)
405
+ throw new Error(`No event for ${plan.lookup.company} ${plan.lookup.period} (GET /v2/events)`);
406
+ console.error(chalk.dim(`resolved ${plan.lookup.company} ${plan.lookup.period} → event ${eventId} (GET /v2/events, ${plan.lookup.credits} credit)`));
407
+ return { request: { ...plan.request, path: plan.request.path.replace('{event_id}', encodeURIComponent(eventId)) } };
408
+ }
409
+ async function saveDownload(operation, body, file, force) {
410
+ const target = operation.download === '' ? body : pick(body, operation.download ?? '');
411
+ const url = isRecord(target) ? target.url : undefined;
412
+ if (typeof url !== 'string' || url === '') {
413
+ throw new Error(`Nothing to save: ${operation.download ? `${operation.download}.url` : 'url'} is empty in this response`);
142
414
  }
143
- const [flag, text] = inline !== undefined
144
- ? ['--body', inline]
145
- : [`--body-file ${file}`, file === '-' ? await readStdin() : await readFile(file ?? '', 'utf8')];
415
+ const record = target;
416
+ const saved = await saveVerified(signal => fetch(url, { signal }), file, {
417
+ force,
418
+ expected: {
419
+ ...(typeof record.sha256 === 'string' ? { sha256: record.sha256 } : {}),
420
+ ...(typeof record.bytes === 'number' ? { bytes: record.bytes } : {}),
421
+ ...(typeof record.content_type === 'string' ? { contentTypes: [record.content_type.split(';')[0]?.trim().toLowerCase() ?? ''] } : {}),
422
+ },
423
+ });
424
+ console.error(chalk.green(`${saved.skipped ? 'Already saved' : 'Saved'} ${saved.path} (${saved.bytes} bytes, sha256 ${saved.sha256})`));
425
+ }
426
+ /** `--all`: every page streamed in the chosen format; a failure after written rows exits 3 with the resume cursor. */
427
+ async function streamAll(request, first, shape, settings) {
428
+ let written = 0;
429
+ let cursor;
430
+ let columns;
431
+ const collected = [];
146
432
  try {
147
- return JSON.parse(text);
433
+ for await (const page of walkPages(request, first)) {
434
+ const note = coverageNote(page.body);
435
+ if (note)
436
+ console.error(chalk.yellow(note));
437
+ if (settings.jq) {
438
+ emit(runJq(settings.jq, page.body).join('\n'));
439
+ written += rowsOf(page.body).length || 1;
440
+ }
441
+ else {
442
+ const rows = bodyRows(shapeBody(page.body, shape, settings), shape);
443
+ if (settings.format === 'csv') {
444
+ columns ??= settings.select ?? settings.prune ?? settings.order ?? csvColumns(rows);
445
+ if (written === 0)
446
+ emit(csvHeader(columns));
447
+ emit(rows.map(row => csvRow(row, columns ?? [])).join('\n'));
448
+ }
449
+ else if (settings.format === 'table' || settings.format === 'plain') {
450
+ collected.push(...bodyRows(page.body, shape));
451
+ }
452
+ else {
453
+ emit(rows.map(row => JSON.stringify(row)).join('\n'));
454
+ }
455
+ written += rows.length;
456
+ }
457
+ cursor = nextCursorOf(page.body);
458
+ }
148
459
  }
149
460
  catch (error) {
150
- throw new Error(`${flag} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
461
+ if (written > 0)
462
+ throw new PartialResultError(error, written, cursor);
463
+ throw error;
464
+ }
465
+ if (settings.format === 'table' || settings.format === 'plain') {
466
+ emit(humanText({ data: collected }, shape, settings), { human: true });
151
467
  }
152
468
  }
153
- function nextUrlOf(operation, data) {
154
- const path = NEXT_URL_PATH[operation.pagination];
155
- const found = path ? pick(data, path) : undefined;
156
- return typeof found === 'string' && found !== '' ? found : undefined;
157
- }
158
- function renderView(operation, data) {
159
- if (operation.view === 'table') {
160
- const found = operation.rowsPath ? pick(data, operation.rowsPath) : data;
161
- const rows = Array.isArray(found) ? found : [];
162
- formatTable(operation.columns ? flattenRows(rows, operation.columns) : rows.filter(isRecord), operation.columns);
163
- printPaginationHint(nextUrlOf(operation, data));
469
+ async function runOperation(operation, positionals, opts, context) {
470
+ const fieldsParam = operation.params.some(param => param.name === 'fields');
471
+ const settings = resolveOutput(opts, { knownFields: operation.fields, fieldsParam, list: operation.list });
472
+ const all = opts.all === true;
473
+ if (opts.save !== undefined && all)
474
+ throw new UsageError('--save saves one response; it cannot be combined with --all');
475
+ let plan = await planRequest(operation, positionals, opts, context);
476
+ if (opts.dryRun) {
477
+ printPlan(operation, plan, opts, all);
164
478
  return;
165
479
  }
166
- if (operation.view === 'keyValue') {
167
- const target = operation.rowsPath ? pick(data, operation.rowsPath) : data;
168
- if (!isRecord(target)) {
169
- formatTable([]);
170
- return;
171
- }
172
- formatKeyValue(operation.columns ? flattenRows([target], operation.columns)[0] : target);
480
+ if (operation.method === 'delete') {
481
+ await confirmOrRefuse(`${describeRequest(plan.request)} deletes this resource.`, opts);
482
+ }
483
+ const threshold = Number(getSetting('confirm_credits').value);
484
+ if (all && Number.isFinite(threshold) && operation.credits >= threshold && threshold > 0) {
485
+ await confirmOrRefuse(`invd ${commandName(operation)} --all costs ${operation.credits} credits per page and may walk many pages. Continue?`, opts);
486
+ }
487
+ plan = await resolveLookup(plan);
488
+ const shape = { list: operation.list, view: operation.view, ...(operation.columns ? { columns: operation.columns } : {}) };
489
+ const first = await apiCall(plan.request);
490
+ if (all) {
491
+ await streamAll(plan.request, first, shape, settings);
173
492
  return;
174
493
  }
175
- formatJson(data);
176
- }
177
- function defineOperationCommand(command, operation) {
178
- const bindings = operation.params.map(param => ({ param, option: paramOption(param) }));
179
- for (const { option } of bindings)
180
- command.addOption(option);
181
- if (operation.body) {
182
- command.option('--body <json>', `JSON request body${operation.body.required ? ' (this or --body-file is required)' : ''}`);
183
- command.option('--body-file <path>', 'Read the JSON request body from a file (- = stdin)');
494
+ const note = coverageNote(first.body);
495
+ if (note)
496
+ console.error(chalk.yellow(note));
497
+ if (first.body === undefined) {
498
+ if (settings.format === 'table' || settings.format === 'plain')
499
+ console.error(chalk.green('Done.'));
500
+ return;
501
+ }
502
+ if (typeof opts.save === 'string')
503
+ await saveDownload(operation, first.body, opts.save, opts.force === true);
504
+ const human = !settings.jq && (settings.format === 'table' || settings.format === 'plain');
505
+ emit(renderBody(first.body, shape, settings), { human });
506
+ const cursor = operation.list ? nextCursorOf(first.body) : undefined;
507
+ if (cursor && human) {
508
+ console.error(chalk.dim(`More rows: --all for every page, or --cursor ${cursor} (same flags) for the next one.`));
184
509
  }
185
- command.action(withErrorHandling(async (_opts, invoked) => {
186
- const globals = invoked.optsWithGlobals();
187
- const json = globals.json === true;
188
- if (json)
189
- setJsonMode(true);
190
- if (globals.all === true && operation.pagination === 'none') {
191
- throw new Error(`invd ${[operation.group, operation.action].filter(Boolean).join(' ')} does not paginate; --all has nothing to fetch`);
192
- }
193
- const values = wireValues(bindings, invoked.opts());
194
- const body = operation.body ? await requestBody(operation, invoked.opts()) : undefined;
195
- const path = operation.path.replace(/\{([^}]+)\}/g, (_match, name) => {
196
- const value = values.get(name);
197
- return encodeURIComponent(typeof value === 'string' ? value : '');
198
- });
199
- const params = {};
200
- for (const param of operation.params) {
201
- if (param.in === 'query')
202
- params[param.name] = values.get(param.name);
203
- }
204
- let data = operation.method === 'get'
205
- ? await apiGet({ path, params, auth: operation.auth })
206
- : await apiRequest({
207
- auth: operation.auth,
208
- method: HTTP_METHOD[operation.method],
209
- path,
210
- params,
211
- ...(body !== undefined ? { body } : {}),
212
- });
213
- if (globals.all === true) {
214
- const itemsPaths = operation.allItemsPath ?? [];
215
- data = operation.pagination === 'top'
216
- ? await fetchAllTopPages(data, itemsPaths)
217
- : await fetchAllCompanyPages(data, itemsPaths);
218
- }
219
- if (data === undefined) {
220
- // A 204: nothing to show but that it worked.
221
- if (!json)
222
- console.log('Done.');
223
- return;
224
- }
225
- output(data, { json }, () => {
226
- renderView(operation, data);
227
- printReadinessNotes(data);
228
- });
229
- }, apiErrorHint));
230
510
  }
231
511
  //# sourceMappingURL=spec-commands.js.map