gangtise-openapi-cli 0.50.0 → 0.52.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 (43) hide show
  1. package/README.md +94 -49
  2. package/dist/src/cli.js +29 -8
  3. package/dist/src/commands/ai.js +27 -12
  4. package/dist/src/commands/alternative.js +2 -2
  5. package/dist/src/commands/bond.js +4 -4
  6. package/dist/src/commands/fund.js +1 -1
  7. package/dist/src/commands/fundamental.js +11 -11
  8. package/dist/src/commands/indicator.js +5 -5
  9. package/dist/src/commands/insight.js +38 -17
  10. package/dist/src/commands/quote.js +6 -6
  11. package/dist/src/commands/raw.js +3 -3
  12. package/dist/src/commands/reference.js +35 -3
  13. package/dist/src/commands/shared.js +20 -8
  14. package/dist/src/commands/tool.js +9 -4
  15. package/dist/src/commands/update.js +11 -0
  16. package/dist/src/commands/vault.js +17 -16
  17. package/dist/src/core/client.js +21 -4
  18. package/dist/src/core/download.js +12 -3
  19. package/dist/src/core/driveUpload.js +6 -2
  20. package/dist/src/core/dryRun.js +68 -0
  21. package/dist/src/core/endpoints.js +87 -6
  22. package/dist/src/core/errors.js +2 -2
  23. package/dist/src/core/etfComponents.js +65 -8
  24. package/dist/src/core/exitStatus.js +4 -0
  25. package/dist/src/core/fileParse.js +10 -3
  26. package/dist/src/core/printer.js +39 -7
  27. package/dist/src/core/statePaths.js +41 -0
  28. package/dist/src/core/transport.js +7 -0
  29. package/gangtise-openapi/SKILL.md +43 -156
  30. package/gangtise-openapi/references/billing.md +142 -0
  31. package/gangtise-openapi/references/commands/ai.md +26 -0
  32. package/gangtise-openapi/references/commands/fund.md +3 -3
  33. package/gangtise-openapi/references/commands/fundamental.md +2 -1
  34. package/gangtise-openapi/references/commands/indicator.md +106 -15
  35. package/gangtise-openapi/references/commands/insight.md +31 -6
  36. package/gangtise-openapi/references/commands/reference-and-lookup.md +42 -4
  37. package/gangtise-openapi/references/commands/tool.md +2 -2
  38. package/gangtise-openapi/references/commands/vault.md +26 -4
  39. package/gangtise-openapi/references/errors.md +3 -3
  40. package/gangtise-openapi/references/examples.md +3 -3
  41. package/gangtise-openapi/references/lookup-ids.md +13 -0
  42. package/gangtise-openapi/references/response-schema.md +8 -2
  43. package/package.json +2 -2
@@ -2,7 +2,7 @@ import { Command, Option } from "commander";
2
2
  import { collectList, collectNames, dateArg, maybeArray, parseNumberOption } from "../core/args.js";
3
3
  import { maxUnitsOf } from "../core/endpoints.js";
4
4
  import { ValidationError } from "../core/errors.js";
5
- import { emit, noteLateStart } from "./shared.js";
5
+ import { emit, noteLateStart, OUTPUT_HELP } from "./shared.js";
6
6
  const BOND_LATE_CAUSE = "The bond may have listed later, or this data starts later than the requested range — the endpoint returns from its first day of data without an error.";
7
7
  export const bond = new Command("bond").description("Bond APIs: profiles, issuers, quotes, valuations, cash flows, ratings, announcements");
8
8
  function requireBondCodes(codes, max) {
@@ -16,7 +16,7 @@ function requireBondCodes(codes, max) {
16
16
  const bondSecurityOption = (command) => command
17
17
  .option("--security <code>", "Bond code, e.g. 019742.SH / 123456.SZ / 220205.IB (repeatable). Short names and pinyin are rejected — resolve them with 'reference securities-search --category bond' first", collectList, [])
18
18
  .option("--field <field>", "Field to return (repeatable); omit for all. An unsupported name rejects the whole call with 100003", collectList, [])
19
- .option("--format <format>", "Output format", "table").option("--output <path>");
19
+ .option("--format <format>", "Output format", "table").option("--output <path>", OUTPUT_HELP);
20
20
  const addBondRange = (name, endpointKey, describe, rangeHelp, max) => bondSecurityOption(bond.command(name).description(describe))
21
21
  .option("--start-date <date>", `${rangeHelp} start (yyyy-MM-dd); omit for all history`, dateArg("--start-date"))
22
22
  .option("--end-date <date>", `${rangeHelp} end (yyyy-MM-dd); omit for all history`, dateArg("--end-date"))
@@ -64,7 +64,7 @@ bond.command("issuance-plan").description("Rate-bond issuance calendar over a da
64
64
  .requiredOption("--start-date <date>", "Issue date range start (yyyy-MM-dd)", dateArg("--start-date"))
65
65
  .requiredOption("--end-date <date>", "Issue date range end (yyyy-MM-dd)", dateArg("--end-date"))
66
66
  .option("--field <field>", "Field to return (repeatable); omit for all", collectList, [])
67
- .option("--format <format>", "Output format", "table").option("--output <path>")
67
+ .option("--format <format>", "Output format", "table").option("--output <path>", OUTPUT_HELP)
68
68
  .action((options) => emit(options, (client) => client.call("bond.issuance-plan", { startDate: options.startDate, endDate: options.endDate, fieldList: maybeArray(options.field) })));
69
69
  bondSecurityOption(bond.command("announcement").description("Bond announcements, paged. Filter by bond codes OR by announcement date range, never both"))
70
70
  .option("--start-date <date>", "Announcement date range start (yyyy-MM-dd); mutually exclusive with --security", dateArg("--start-date"))
@@ -90,7 +90,7 @@ bond.command("default-list").description("Defaulted bonds: by default-date range
90
90
  .option("--security <code>", "Bond code, e.g. 169687.SH / 220205.IB (repeatable, no per-call limit)", collectList, [])
91
91
  .option("--issuer <name>", "One issuer, by full name. A short name is matched to a single closest issuer, which may not be the one meant — check issuerName in the result", collectNames, [])
92
92
  .option("--field <field>", "Field to return (repeatable); omit for all. An unsupported name rejects the whole call with 100005", collectList, [])
93
- .option("--format <format>", "Output format", "table").option("--output <path>")
93
+ .option("--format <format>", "Output format", "table").option("--output <path>", OUTPUT_HELP)
94
94
  .action((options) => {
95
95
  const byDate = Boolean(options.startDate || options.endDate);
96
96
  const modes = [byDate, options.security.length > 0, options.issuer.length > 0].filter(Boolean).length;
@@ -3,7 +3,7 @@ import { collectNames } from "../core/args.js";
3
3
  import { addComponentCodes, clientLookup } from "../core/etfComponents.js";
4
4
  import { date, flag, format, list, output, query, requiredList, value } from "./shared.js";
5
5
  export const fund = new Command("fund").description("Fund APIs: profiles, NAV, fees, managers, scale, holders, holdings and allocation, ETF creation/redemption");
6
- const code = () => requiredList("--security <code>", "Fund code, e.g. 005827.OF / 159967.SZ / 510300.SH (repeatable, comma-separated accepted). From a fund name: 'reference securities-search --category fund', keeping only rows whose category is fund", "fundCodeList");
6
+ const code = () => requiredList("--security <code>", "Fund code, e.g. 005827.OF / 159967.SZ / 510300.SH (repeatable, comma-separated accepted). From a fund name: 'reference securities-search --category fund', which returns funds only", "fundCodeList");
7
7
  const range = (what) => [
8
8
  date("--start-date <date>", `${what} range start (yyyy-MM-dd); omit both ends for everything in your account's history window`, "startDate"),
9
9
  date("--end-date <date>", `${what} range end (yyyy-MM-dd)`, "endDate"),
@@ -4,16 +4,16 @@ import { COSTLY_FETCH_CREDITS } from "../core/endpoints.js";
4
4
  import { normalizeRows } from "../core/normalize.js";
5
5
  import { parseOutputFormat } from "../core/output.js";
6
6
  import { printData } from "../core/printer.js";
7
- import { emit, withClient, flagIfLimitTruncated, field, required, date, list, format, output, query } from "./shared.js";
7
+ import { emit, withClient, flagIfLimitTruncated, field, date, list, format, output, query, OUTPUT_HELP, SECURITY_CODE_FLAGS, SECURITY_CODE_HELP, oneSecurityCode, securityCode } from "./shared.js";
8
8
  export const fundamental = new Command("fundamental").description("Fundamental APIs");
9
- const addFinancialReport = (name, endpointKey, periodHelp = "Period") => query(fundamental, name, {
9
+ const addFinancialReport = (name, endpointKey, periodHelp = "Period: q1/interim/q3/annual/latest") => query(fundamental, name, {
10
10
  endpoint: endpointKey,
11
11
  fields: [
12
- required("--security-code <code>", undefined, "securityCode"),
12
+ securityCode(),
13
13
  date("--start-date <date>", "Start date (yyyy-MM-dd)", "startDate"),
14
14
  date("--end-date <date>", "End date (yyyy-MM-dd)", "endDate"),
15
- list("--fiscal-year <year>", "Fiscal year", "fiscalYear"),
16
- list("--period <period>", periodHelp, "period"),
15
+ list("--fiscal-year <year>", "Fiscal year; pass --period with it, or the latest report comes back whatever the year", "fiscalYear"),
16
+ list("--period <period>", `${periodHelp} (server default: latest)`, "period"),
17
17
  list("--report-type <type>", "Report type", "reportType"),
18
18
  list("--field <field>", "Field", "fieldList"),
19
19
  format(), output(),
@@ -33,7 +33,7 @@ addFinancialReport("cash-flow-us", "fundamental.cash-flow-us", "Period: q1/h1/q3
33
33
  query(fundamental, "main-business", {
34
34
  endpoint: "fundamental.main-business",
35
35
  fields: [
36
- required("--security-code <code>", undefined, "securityCode"),
36
+ securityCode(),
37
37
  date("--start-date <date>", "Start date (yyyy-MM-dd)", "startDate"),
38
38
  date("--end-date <date>", "End date (yyyy-MM-dd)", "endDate"),
39
39
  field(new Option("--breakdown <type>", "Breakdown: product/industry/region").choices(["product", "industry", "region"]).default("product"), (v) => ({ breakdown: v })),
@@ -62,7 +62,7 @@ function firstRowTradeDate(data) {
62
62
  return typeof value === "string" ? value : undefined;
63
63
  }
64
64
  fundamental.command("valuation-analysis")
65
- .requiredOption("--security-code <code>")
65
+ .requiredOption(SECURITY_CODE_FLAGS, SECURITY_CODE_HELP, oneSecurityCode)
66
66
  .addOption(new Option("--indicator <name>", "Indicator").choices(["peTtm", "pbMrq", "peg", "psTtm", "pcfTtm", "em"]).makeOptionMandatory())
67
67
  .option("--start-date <date>", "Start date (yyyy-MM-dd)", dateArg("--start-date"))
68
68
  .option("--end-date <date>", "End date (yyyy-MM-dd)", dateArg("--end-date"))
@@ -70,7 +70,7 @@ fundamental.command("valuation-analysis")
70
70
  .option("--field <field>", "Field", collectList, [])
71
71
  .option("--skip-null", "Drop rows where value or percentileRank is null")
72
72
  .option("--format <format>", "Output format", "table")
73
- .option("--output <path>")
73
+ .option("--output <path>", OUTPUT_HELP)
74
74
  .action((options) => withClient(async (client) => {
75
75
  const format = parseOutputFormat(options.format);
76
76
  const requested = maybeArray(options.field);
@@ -115,7 +115,7 @@ fundamental.command("valuation-analysis")
115
115
  query(fundamental, "top-holders", {
116
116
  endpoint: "fundamental.top-holders",
117
117
  fields: [
118
- required("--security-code <code>", undefined, "securityCode"),
118
+ securityCode(),
119
119
  field(new Option("--holder-type <type>", "Holder type: top10/top10Float").choices(["top10", "top10Float"]).makeOptionMandatory(), (v) => ({ holderType: v })),
120
120
  date("--start-date <date>", "Start date (yyyy-MM-dd)", "startDate"),
121
121
  date("--end-date <date>", "End date (yyyy-MM-dd)", "endDate"),
@@ -125,13 +125,13 @@ query(fundamental, "top-holders", {
125
125
  ],
126
126
  });
127
127
  fundamental.command("earning-forecast")
128
- .requiredOption("--security-code <code>")
128
+ .requiredOption(SECURITY_CODE_FLAGS, SECURITY_CODE_HELP, oneSecurityCode)
129
129
  .option("--start-date <date>", "Start date (default: 1 year before end-date)", dateArg("--start-date"))
130
130
  .option("--end-date <date>", "End date (default: today)", dateArg("--end-date"))
131
131
  .option("--consensus <name>", "Consensus indicator: netIncome/netIncomeYoy/eps/pe/bps/pb/peg/roe/ps", collectList, [])
132
132
  .option("--yes", `Fetch a range estimated above ${COSTLY_FETCH_CREDITS} credits (billed per row; see the refusal for the estimate)`)
133
133
  .option("--format <format>", "Output format", "table")
134
- .option("--output <path>")
134
+ .option("--output <path>", OUTPUT_HELP)
135
135
  .action((options) => emit(options, (client) => {
136
136
  const endDate = options.endDate ?? beijingDateString(new Date());
137
137
  const startDate = options.startDate ?? new Date(new Date(`${endDate}T00:00:00Z`).getTime() - 365 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
@@ -5,7 +5,7 @@ import { checkScreenerBindings, droppedFromMatrix, flattenCrossSection, flattenT
5
5
  import { ValidationError } from "../core/errors.js";
6
6
  import { parseOutputFormat } from "../core/output.js";
7
7
  import { printData } from "../core/printer.js";
8
- import { withClient } from "./shared.js";
8
+ import { withClient, OUTPUT_HELP } from "./shared.js";
9
9
  function flagDropped(rows, data, requestedSecurities, requestedIndicators, timeSeries = false) {
10
10
  if (isEmptyMatrix(data)) {
11
11
  if (timeSeries) {
@@ -44,7 +44,7 @@ indicator.command("search")
44
44
  .requiredOption("--keyword <text>", "Search keyword, e.g. '收盘价' '成交量' '营业收入' (not free-form questions)")
45
45
  .option("--limit <number>", "Max results (default: 50, max: 100)", "50")
46
46
  .option("--format <format>", "Output format", "table")
47
- .option("--output <path>")
47
+ .option("--output <path>", OUTPUT_HELP)
48
48
  .action((options) => withClient(async (client) => {
49
49
  const format = parseOutputFormat(options.format);
50
50
  const raw = await client.call("indicator.search", {
@@ -62,7 +62,7 @@ indicator.command("cross-section")
62
62
  .option("--indicator-param <spec>", "Per-indicator param 'code:key=value', e.g. qte_close:adjustType=2 for 前复权 (repeat); read exact keys from 'indicator search'. Bare 'code:' (nothing after the colon) declares the indicator takes NO date — required by any indicator whose parameterList has no date key at all, which otherwise rejects the tradeDate --date injects: the pty_* / scr_* static-attribute families (pty_op_scope, scr_exchg_mkt, scr_isin …), plus div_cash_paid_ratio / div_cash_yr (add 'code:fiscalYear=YYYY' too) and pty_shr_reg. It composes with real params, so 'code:' + 'code:scale=8' keeps the scale", collectNames, [])
63
63
  .addOption(new Option("--key-by <mode>", "Column key: name=display name (default) | code=indicatorCode, unique & order-stable for batch code→value mapping").choices(["name", "code"]).default("name"))
64
64
  .option("--format <format>", "Output format", "table")
65
- .option("--output <path>")
65
+ .option("--output <path>", OUTPUT_HELP)
66
66
  .action((options) => withClient(async (client) => {
67
67
  const format = parseOutputFormat(options.format);
68
68
  requireIndicatorScope(options.indicator, options.security);
@@ -83,7 +83,7 @@ indicator.command("time-series")
83
83
  .option("--indicator-param <spec>", "Per-indicator param 'code:key=value', e.g. qte_close:adjustType=2 for 前复权 (repeat); read exact keys from 'indicator search'", collectNames, [])
84
84
  .addOption(new Option("--key-by <mode>", "Column key: name=display name (default) | code=indicatorCode/securityCode, unique & order-stable for batch mapping").choices(["name", "code"]).default("name"))
85
85
  .option("--format <format>", "Output format", "table")
86
- .option("--output <path>")
86
+ .option("--output <path>", OUTPUT_HELP)
87
87
  .action((options) => withClient(async (client) => {
88
88
  const format = parseOutputFormat(options.format);
89
89
  requireIndicatorScope(options.indicator, options.security);
@@ -102,7 +102,7 @@ indicator.command("screener")
102
102
  .option("--indicator-param <spec>", "Per-variable param 'F1:key=value', e.g. F1:scale=8 (repeat); read exact keys from 'indicator search'. Bare 'F1:' (nothing after the colon) declares that the indicator takes NO date — required by any indicator whose parameterList has no date key at all, which otherwise rejects the tradeDate --date injects: the pty_* / scr_* static-attribute families (pty_op_scope, scr_exchg_sctr, scr_isin …), plus div_cash_paid_ratio / div_cash_yr (add 'F1:fiscalYear=YYYY' too) and pty_shr_reg. It composes with real params, so 'F1:' + 'F1:scale=8' keeps the scale", collectNames, [])
103
103
  .addOption(new Option("--key-by <mode>", "Column key: name=display name (default) | code=indicatorCode").choices(["name", "code"]).default("name"))
104
104
  .option("--format <format>", "Output format", "table")
105
- .option("--output <path>")
105
+ .option("--output <path>", OUTPUT_HELP)
106
106
  .action((options) => withClient(async (client) => {
107
107
  const format = parseOutputFormat(options.format);
108
108
  requireIndicatorScope(options.indicator, options.security);
@@ -3,7 +3,7 @@ import { collectList, dateArg, datetimeArg, maybeArray, parseChoiceList, parseEn
3
3
  import { ValidationError } from "../core/errors.js";
4
4
  import { fetchOpinionDetails } from "../core/opinionDetail.js";
5
5
  import { rowCount } from "../core/rowSink.js";
6
- import { emit, addDownloadCommand, confirmCostly, multiChoice, field, value, required, list, numberList, choiceList, count, rankType, searchType, top, flag, format, output, from, size, startTime, endTime, timeFilters, query, RESEARCH_AREA_CITIC_OR_DIRECTION } from "./shared.js";
6
+ import { emit, addDownloadCommand, confirmCostly, multiChoice, field, value, required, list, numberList, choiceList, count, rankType, searchType, top, flag, format, output, from, size, startTime, endTime, timeFilters, query, RESEARCH_AREA_CITIC_OR_DIRECTION, OUTPUT_HELP, securityCode } from "./shared.js";
7
7
  export const insight = new Command("insight").description("Insight APIs");
8
8
  const SW_INDUSTRY_ONLY = "Industry ID -- swIndustry codes only (104xx0000); citicIndustry codes are rejected with 100005 even where constant-category declares them";
9
9
  const opinion = new Command("opinion");
@@ -21,6 +21,7 @@ const announcementHk = new Command("announcement-hk");
21
21
  const announcementUs = new Command("announcement-us");
22
22
  const foreignOpinion = new Command("foreign-opinion");
23
23
  const independentOpinion = new Command("independent-opinion");
24
+ const overseasOpinion = new Command("overseas-opinion");
24
25
  const officialAccount = new Command("official-account");
25
26
  const qa = new Command("qa");
26
27
  const reportImage = new Command("report-image");
@@ -46,7 +47,7 @@ query(opinion, "list", {
46
47
  list("--broker-tag <tag>", "Broker tag: topBroker/newFortune", "brokerTagList"),
47
48
  list("--source <source>", "Source", "sourceList"),
48
49
  flag("--with-content", "Return the full body inline (v1 list, 30 credits/row) instead of the 200-char brief (1 credit/row). v1 shape: title and body sit under contentList.title / contentList.content, and there is no brief. Takes no --category / --broker-tag, and --llm-tag only strongRcmd/earningsReview/topBroker/newFortune"),
49
- format(), output("Output path"),
50
+ format(), output(),
50
51
  ...timeFilters(),
51
52
  ],
52
53
  });
@@ -62,7 +63,7 @@ query(summary, "list", {
62
63
  list("--category <name>", "Category", "categoryList"),
63
64
  list("--market <name>", "Market", "marketList"),
64
65
  list("--participant-role <name>", "Participant role", "participantRoleList"),
65
- format(), output("Output path"),
66
+ format(), output(),
66
67
  ...timeFilters(),
67
68
  ],
68
69
  });
@@ -79,7 +80,7 @@ query(pamirsSummary, "list", {
79
80
  list("--security <code>", "Security code, e.g. 000001.SZ", "securityList"),
80
81
  choiceList("--category <name>", `Category: ${PAMIRS_CATEGORIES.join(" / ")}`, "categoryList", PAMIRS_CATEGORIES),
81
82
  choiceList("--market <name>", `Market: ${PAMIRS_MARKETS.join(" / ")}`, "marketList", PAMIRS_MARKETS),
82
- format(), output("Output path"),
83
+ format(), output(),
83
84
  ...timeFilters(),
84
85
  ],
85
86
  });
@@ -102,7 +103,7 @@ const addScheduleList = (command, endpointKey, fields) => query(command, "list",
102
103
  fields.brokerType && list("--broker-type <name>", "Lead broker type: cnBroker/otherBroker", "brokerTypeList"),
103
104
  fields.permission && numberList("--permission <number>", "Permission: 1=public 2=private", "permission"),
104
105
  fields.location && list("--location <id>", "Location ID (domesticCity constant, via 'reference constant-list')", "locationList"),
105
- format(), output("Output path"),
106
+ format(), output(),
106
107
  ...timeFilters(),
107
108
  publishTime("--publish-start-time <datetime>", "Collected from (publishTime, when the schedule first came in — not when it is held); combines with --start-time/--end-time", "publishStartTime"),
108
109
  publishTime("--publish-end-time <datetime>", "Collected until (publishTime); a bare date covers that whole day", "publishEndTime"),
@@ -146,7 +147,7 @@ performanceCalendar.command("list").description("Earnings calendar (业绩预告
146
147
  .option("--security <code>", "Security code (e.g. 000001.SZ)", collectList, [])
147
148
  .addOption(multiChoice("--market <name>", `Market: ${PERFORMANCE_MARKETS.join("/")}`, PERFORMANCE_MARKETS))
148
149
  .addOption(multiChoice("--category <name>", `Event type: ${PERFORMANCE_CATEGORIES.join("/")}`, PERFORMANCE_CATEGORIES))
149
- .option("--format <format>", "Output format", "table").option("--output <path>", "Output path")
150
+ .option("--format <format>", "Output format", "table").option("--output <path>", OUTPUT_HELP)
150
151
  .addOption(confirmCostly().option)
151
152
  .action((options) => {
152
153
  const marketList = parseChoiceList(options.market, "--market", PERFORMANCE_MARKETS);
@@ -186,7 +187,7 @@ query(research, "list", {
186
187
  count("--min-pages <number>", "Min report pages", "minReportPages"),
187
188
  count("--max-pages <number>", "Max report pages", "maxReportPages"),
188
189
  list("--source <type>", "Source type", "sourceList"),
189
- format(), output("Output path"),
190
+ format(), output(),
190
191
  ...timeFilters(),
191
192
  ],
192
193
  });
@@ -206,7 +207,7 @@ query(foreignReport, "list", {
206
207
  list("--rating-change <name>", "Rating change", "ratingChangeList"),
207
208
  count("--min-pages <number>", "Min report pages", "minReportPages"),
208
209
  count("--max-pages <number>", "Max report pages", "maxReportPages"),
209
- format(), output("Output path"),
210
+ format(), output(),
210
211
  ...timeFilters(),
211
212
  ],
212
213
  });
@@ -218,7 +219,7 @@ query(announcement, "list", {
218
219
  searchType(), rankType(),
219
220
  list("--security <code>", "Security code", "securityList"),
220
221
  list("--category <id>", "Category ID", "categoryList"),
221
- format(), output("Output path"),
222
+ format(), output(),
222
223
  ...timeFilters(parseTimestamp13, parseEndTimestamp13),
223
224
  ],
224
225
  });
@@ -230,7 +231,7 @@ query(announcementHk, "list", {
230
231
  searchType(), rankType(),
231
232
  list("--security <code>", "Security code (e.g. 01913.HK)", "securityList"),
232
233
  list("--category <id>", "Category ID", "categoryList"),
233
- format(), output("Output path"),
234
+ format(), output(),
234
235
  ...timeFilters(),
235
236
  ],
236
237
  });
@@ -242,7 +243,7 @@ query(announcementUs, "list", {
242
243
  searchType(), rankType(),
243
244
  list("--security <code>", "Security code (e.g. TSLA.O)", "securityList"),
244
245
  list("--category <id>", "Category ID (constant-list usShareAnnouncementCategory)", "categoryList"),
245
- format(), output("Output path"),
246
+ format(), output(),
246
247
  ...timeFilters(),
247
248
  ],
248
249
  });
@@ -258,17 +259,17 @@ query(foreignOpinion, "list", {
258
259
  list("--rating <name>", "Rating", "ratingList"),
259
260
  list("--rating-change <name>", "Rating change", "ratingChangeList"),
260
261
  flag("--with-content", "Return the full body inline (v1 list, 30 credits/row) as content / contentTranslate, instead of the 200-char brief (1 credit/row)"),
261
- format(), output("Output path"),
262
+ format(), output(),
262
263
  ...timeFilters(),
263
264
  ],
264
265
  });
265
266
  opinion.command("detail").description("Full bodies of domestic chief opinions by ID (30 credits per returned opinion; batched 20 per call)")
266
267
  .requiredOption("--chief-opinion-id <id>", "chiefOpinionId from 'insight opinion list' (repeat or comma-separate)", collectList)
267
- .option("--format <format>", "Output format", "json").option("--output <path>")
268
+ .option("--format <format>", "Output format", "json").option("--output <path>", OUTPUT_HELP)
268
269
  .action((options) => emit(options, (client) => fetchOpinionDetails(client, "insight.opinion.detail", "chiefOpinionIdList", "chiefOpinionId", options.chiefOpinionId)));
269
270
  foreignOpinion.command("detail").description("Full bodies (content + contentTranslate) of foreign opinions by ID (30 credits per returned opinion; batched 20 per call)")
270
271
  .requiredOption("--foreign-opinion-id <id>", "foreignOpinionId from 'insight foreign-opinion list' (repeat or comma-separate)", collectList)
271
- .option("--format <format>", "Output format", "json").option("--output <path>")
272
+ .option("--format <format>", "Output format", "json").option("--output <path>", OUTPUT_HELP)
272
273
  .action((options) => emit(options, (client) => fetchOpinionDetails(client, "insight.foreign-opinion.detail", "foreignOpinionIdList", "foreignOpinionId", options.foreignOpinionId)));
273
274
  query(independentOpinion, "list", {
274
275
  endpoint: "insight.independent-opinion.list",
@@ -278,11 +279,30 @@ query(independentOpinion, "list", {
278
279
  list("--industry <id>", SW_INDUSTRY_ONLY, "industryList"),
279
280
  list("--rating <name>", "Rating", "ratingList"),
280
281
  list("--rating-change <name>", "Rating change", "ratingChangeList"),
281
- format(), output("Output path"),
282
+ format(), output(),
282
283
  ...timeFilters(),
283
284
  ],
284
285
  });
285
286
  addDownloadCommand(independentOpinion, { endpointKey: "insight.independent-opinion.download", idOption: "--independent-opinion-id", idField: "independentOpinionId", fallbackPrefix: "independent-opinion", fileType: { description: "File type: 1=original HTML 2=CN-translated HTML", choices: ["1", "2"], required: true } });
287
+ query(overseasOpinion, "list", {
288
+ description: "List overseas opinions: brief + briefTranslate, body via 'overseas-opinion detail' (0.1 credits/row)",
289
+ endpoint: "insight.overseas-opinion.list",
290
+ fields: [
291
+ rankType(),
292
+ list("--account <id>", "Account ID from 'reference overseas-account-search' (repeat or comma-separate)", "accountIdList"),
293
+ list("--security <code>", "Security code with market suffix, e.g. NVDA.O. A missing suffix is rejected (120001); a real suffix of the wrong market (NVDA.N) matches nothing", "securityList"),
294
+ list("--category <name>", "Category: opinion/news/dataAnalysis/promotion (repeat). A row in several categories is returned under each; rows without a category only when this is omitted", "categoryList"),
295
+ list("--industry <id>", "GICS sector ID: 107700001 energy ... 107700011 real estate (list: 'reference constant-list --category gicsIndustry'). Other industry code sets are rejected (100005)", "industryList"),
296
+ list("--region <code>", "Region code: cn/cnHk/cnTw/us/jp/kr/uk/gl; other regionCategory values are rejected (100005)", "regionList"),
297
+ count("--original-flag <number>", "1=original posts only 0=reposts only; omit for both", "originalFlag"),
298
+ format(), output(),
299
+ ...timeFilters(),
300
+ ],
301
+ });
302
+ overseasOpinion.command("detail").description("Full bodies (content + contentTranslate) of overseas opinions by ID (10 credits per returned opinion; batched 20 per call)")
303
+ .requiredOption("--overseas-opinion-id <id>", "overseasOpinionId from 'insight overseas-opinion list' (repeat or comma-separate)", collectList)
304
+ .option("--format <format>", "Output format", "json").option("--output <path>", OUTPUT_HELP)
305
+ .action((options) => emit(options, (client) => fetchOpinionDetails(client, "insight.overseas-opinion.detail", "overseasOpinionIdList", "overseasOpinionId", options.overseasOpinionId)));
286
306
  query(officialAccount, "list", {
287
307
  endpoint: "insight.official-account.list",
288
308
  cache: { endpointKey: "insight.official-account.list", idField: "articleId" },
@@ -292,7 +312,7 @@ query(officialAccount, "list", {
292
312
  list("--security <code>", "Security code (e.g. 000001.SZ)", "securityList"),
293
313
  list("--category <type>", "Article type: news/law/report/view/data/event/meeting/notice/recruit/investEdu/brand/notes/other", "categoryList"),
294
314
  list("--industry <id>", "Industry ID (constant-list citicIndustry/swIndustry)", "industryList"),
295
- format(), output("Output path"),
315
+ format(), output(),
296
316
  ...timeFilters(),
297
317
  ],
298
318
  });
@@ -300,7 +320,7 @@ addDownloadCommand(officialAccount, { endpointKey: "insight.official-account.dow
300
320
  query(qa, "list", {
301
321
  endpoint: "insight.qa.list",
302
322
  fields: [
303
- required("--security-code <code>", "Security code, e.g. 601012.SH", "securityCode"),
323
+ securityCode("Security code, e.g. 601012.SH"),
304
324
  from(), size("Total rows to return; omit to fetch all (max page 500)"),
305
325
  startTime("Start time (yyyy-MM-dd or yyyy-MM-dd HH:mm:ss)"), endTime("End time (yyyy-MM-dd or yyyy-MM-dd HH:mm:ss)"),
306
326
  list("--source <type>", "Source: conference/interactive/survey (repeat)", "source"),
@@ -335,6 +355,7 @@ insight.addCommand(announcementHk);
335
355
  insight.addCommand(announcementUs);
336
356
  insight.addCommand(foreignOpinion);
337
357
  insight.addCommand(independentOpinion);
358
+ insight.addCommand(overseasOpinion);
338
359
  insight.addCommand(officialAccount);
339
360
  insight.addCommand(qa);
340
361
  insight.addCommand(reportImage);
@@ -8,7 +8,7 @@ import { flagMissingFields } from "../core/normalize.js";
8
8
  import { parseOutputFormat } from "../core/output.js";
9
9
  import { printData } from "../core/printer.js";
10
10
  import { rowCount } from "../core/rowSink.js";
11
- import { emit, withClient, DEFAULT_QUOTE_LIMIT, MAX_QUOTE_LIMIT, flagIfLimitTruncated, noteLateStart } from "./shared.js";
11
+ import { emit, withClient, DEFAULT_QUOTE_LIMIT, MAX_QUOTE_LIMIT, flagIfLimitTruncated, noteLateStart, OUTPUT_HELP } from "./shared.js";
12
12
  export const quote = new Command("quote").description("Quote APIs");
13
13
  const KLINE_MARKETS = { aShares: 1, hkStocks: 2, usStocks: 1 };
14
14
  const LEGACY_ALL_MARKET = (shardDays) => ({ all: shardDays });
@@ -48,7 +48,7 @@ const addKlineCommand = (name, endpointKey, securityHelp, markets, noKeywordReas
48
48
  .option("--limit <number>", `Max rows per request (default: ${DEFAULT_QUOTE_LIMIT}, max: ${MAX_QUOTE_LIMIT}). With several securities it also sizes the groups they are fetched in, so a small value fetches one security per request`)
49
49
  .option("--field <field>", "Field", collectList, [])
50
50
  .option("--format <format>", "Output format", "table")
51
- .option("--output <path>")
51
+ .option("--output <path>", OUTPUT_HELP)
52
52
  .action((options) => {
53
53
  checkMarketKeywords(options.security, Object.keys(markets), `quote ${name}`, noKeywordReason);
54
54
  options.security = canonicalizeMarketKeywords(options.security, Object.keys(markets));
@@ -100,7 +100,7 @@ quote.command("minute-kline")
100
100
  .option("--limit <number>", `Max rows per request (default: ${DEFAULT_QUOTE_LIMIT}, max: ${MAX_QUOTE_LIMIT})`)
101
101
  .option("--field <field>", "Field", collectList, [])
102
102
  .option("--format <format>", "Output format", "table")
103
- .option("--output <path>")
103
+ .option("--output <path>", OUTPUT_HELP)
104
104
  .action((options) => withClient(options, async (client) => {
105
105
  const format = parseOutputFormat(options.format);
106
106
  client.rowSink?.watchEarliest("tradeTime");
@@ -127,7 +127,7 @@ quote.command("realtime")
127
127
  .option("--security <code>", "Security code — stock .SH/.SZ/.BJ/.HK/.O/.N/.A, ETF .SH/.SZ (e.g. 512800.SH), exchange index .SH/.SZ/.BJ, concept index .GT, industry index .CI/.SWI, global index (e.g. SPX.SPI / N225.NKI / HSI.HI); or one market keyword: aShares / hkStocks / usStocks (must be passed alone; keywords cover stocks only — ETFs and indices have no whole-market keyword)", collectList, [])
128
128
  .option("--field <field>", "Field", collectList, [])
129
129
  .option("--format <format>", "Output format", "table")
130
- .option("--output <path>")
130
+ .option("--output <path>", OUTPUT_HELP)
131
131
  .action((options) => {
132
132
  checkMarketKeywords(options.security, REALTIME_MARKETS, "quote realtime");
133
133
  return emit(options, async (client) => {
@@ -147,7 +147,7 @@ quote.command("fund-flow")
147
147
  .option("--limit <number>", `Max rows per request (default: ${DEFAULT_QUOTE_LIMIT}, max: ${MAX_QUOTE_LIMIT}; single-security cap — aShares auto-shards by day)`)
148
148
  .option("--field <field>", "Field, e.g. mainNetInflow/largeInflow/xlargeOutflow (repeat); omit for all", collectList, [])
149
149
  .option("--format <format>", "Output format", "table")
150
- .option("--output <path>")
150
+ .option("--output <path>", OUTPUT_HELP)
151
151
  .action((options) => {
152
152
  checkMarketKeywords(options.security, FUND_FLOW_MARKETS, "quote fund-flow");
153
153
  options.security = canonicalizeMarketKeywords(options.security, FUND_FLOW_MARKETS);
@@ -187,7 +187,7 @@ quote.command("sector-fund-flow")
187
187
  .option("--limit <number>", `Max rows returned (default: ${DEFAULT_QUOTE_LIMIT}, max: ${MAX_QUOTE_LIMIT}). It cuts the answer only: a query too heavy to compute fails with 100006 whatever the limit — shorten the range, or pass fewer sectors or fields`)
188
188
  .option("--field <field>", "Field, e.g. sectorMainNetInflow / sectorLargeInflow (repeat); omit for all. sectorId and tradeDate always come first", collectList, [])
189
189
  .option("--format <format>", "Output format", "table")
190
- .option("--output <path>")
190
+ .option("--output <path>", OUTPUT_HELP)
191
191
  .action((options) => emit(options, async (client) => {
192
192
  const limit = parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1, max: MAX_QUOTE_LIMIT }) ?? DEFAULT_QUOTE_LIMIT;
193
193
  const fieldList = maybeArray(options.field);
@@ -5,8 +5,8 @@ import { ConfigError, ValidationError } from "../core/errors.js";
5
5
  import { flagFailedItems } from "../core/normalize.js";
6
6
  import { parseOutputFormat } from "../core/output.js";
7
7
  import { printData } from "../core/printer.js";
8
- import { createClient, runDownload, assertConfirmed } from "./shared.js";
9
- export const raw = new Command("raw").description("Raw API calls").addCommand(new Command("call").argument("<endpointKey>").option("--body <json>").option("--query <key=value>", "Query string pair", collectKeyValue, {}).option("--yes", "Confirm an irreversible endpoint (required for the ones marked destructive), or fetch every row of a per-row billed list past the credit guard").option("--format <format>", "Output format", "json").option("--output <path>").action(async (endpointKey, options) => {
8
+ import { createClient, runDownload, assertConfirmed, OUTPUT_HELP } from "./shared.js";
9
+ export const raw = new Command("raw").description("Raw API calls").addCommand(new Command("call").argument("<endpointKey>").option("--body <json>", "Request body as a JSON string").option("--query <key=value>", "Query string pair", collectKeyValue, {}).option("--yes", "Confirm an irreversible endpoint (required for the ones marked destructive), or fetch every row of a per-row billed list past the credit guard").option("--format <format>", "Output format", "json").option("--output <path>", OUTPUT_HELP).action(async (endpointKey, options) => {
10
10
  const endpoint = ENDPOINTS[endpointKey];
11
11
  if (!endpoint) {
12
12
  throw new ConfigError(`Unknown endpoint key: ${endpointKey}`);
@@ -47,4 +47,4 @@ export const raw = new Command("raw").description("Raw API calls").addCommand(ne
47
47
  finally {
48
48
  await client.rowSink?.abort();
49
49
  }
50
- })).addCommand(new Command("list").description("List all registered endpoint keys (for use with 'raw call')").option("--format <format>", "Output format", "table").option("--output <path>").action((options) => printData(listEndpoints(), parseOutputFormat(options.format), options.output)));
50
+ })).addCommand(new Command("list").description("List all registered endpoint keys (for use with 'raw call')").option("--format <format>", "Output format", "table").option("--output <path>", OUTPUT_HELP).action((options) => printData(listEndpoints(), parseOutputFormat(options.format), options.output)));
@@ -1,11 +1,11 @@
1
1
  import { Command } from "commander";
2
- import { value, required, choiceList, top, format, output, query } from "./shared.js";
2
+ import { value, required, requiredList, date, choiceList, top, format, output, from, size, query } from "./shared.js";
3
3
  export const reference = new Command("reference").description("Reference data APIs");
4
4
  query(reference, "securities-search", {
5
5
  endpoint: "reference.securities-search",
6
6
  fields: [
7
7
  required("--keyword <text>", "Search keyword (name/code/pinyin/English)", "keyword"),
8
- choiceList("--category <type>", "Category: stock/dr/index/fund/bond. Convertible bonds can come back under any category — keep the rows whose returned category matches", "category", ["stock", "dr", "index", "fund", "bond"]),
8
+ choiceList("--category <type>", "Category: stock/dr/index/fund/bond. Without it, a convertible bond can rank above its stock on a short name", "category", ["stock", "dr", "index", "fund", "bond"]),
9
9
  top(10),
10
10
  format(), output(),
11
11
  ],
@@ -34,6 +34,29 @@ query(reference, "sector-constituents", {
34
34
  endpoint: "reference.sector-constituents",
35
35
  fields: [required("--sector-id <id>", "Sector ID from 'reference sector-search'", "sectorId"), format(), output()],
36
36
  });
37
+ function noteMissingIndices(data, requested, tradeDate) {
38
+ const rows = data?.list;
39
+ if (!Array.isArray(rows))
40
+ return;
41
+ const found = new Set(rows.map((row) => String(row?.indexCode).toUpperCase()));
42
+ const missing = requested.filter((code) => !found.has(code.toUpperCase()));
43
+ if (!missing.length)
44
+ return;
45
+ process.stderr.write(`[gangtise] note: no constituent weights for ${missing.join(", ")} on ${tradeDate}. Weights come for the last trading day of each month — another date, like an unknown index code, comes back empty without an error. Check the date, and the code with 'reference securities-search --category index'.\n`);
46
+ }
47
+ query(reference, "index-weight", {
48
+ description: "Index constituents and their weights (%) on the last trading day of a month (free)",
49
+ endpoint: "reference.index-weight",
50
+ fields: [
51
+ requiredList("--index <code>", "Index code, e.g. 000300.SH / 000905.SH / 399006.SZ (repeat or comma-separate)", "indexCodeList"),
52
+ date("--date <date>", "Trading date (yyyy-MM-dd): the last trading day of a month, e.g. 2026-10-30; any other date returns no rows", "tradeDate", { required: true }),
53
+ format(), output(),
54
+ ],
55
+ shape: async (data, _client, options) => {
56
+ noteMissingIndices(data, options.index, options.date);
57
+ return data;
58
+ },
59
+ });
37
60
  query(reference, "chiefs-search", {
38
61
  endpoint: "reference.chiefs-search",
39
62
  fields: [required("--keyword <text>", "Search keyword (chief name / institution / team)", "keyword"), top(10), format(), output()],
@@ -50,9 +73,18 @@ query(reference, "institution-search", {
50
73
  query(reference, "official-account-search", {
51
74
  endpoint: "reference.official-account-search",
52
75
  fields: [
53
- required("--keyword <text>", "Search keyword (account name / institution / keyword, e.g. 东吴证券)", "keyword"),
76
+ required("--keyword <text>", "Account name fragment, e.g. 中信证券 (finds 中信证券研究). Matches the account name: an institution's full name misses accounts named by short form (东吴证券 finds none, 东吴 finds 东吴策略陈刚团队)", "keyword"),
54
77
  choiceList("--category <name>", "Category: listedCompany/broker/government/media (repeat); omit for all incl. uncategorized", "category", ["listedCompany", "broker", "government", "media"]),
55
78
  top(10),
56
79
  format(), output(),
57
80
  ],
58
81
  });
82
+ query(reference, "overseas-account-search", {
83
+ description: "Search overseas opinion account IDs by name fragment, for 'insight overseas-opinion list --account' (free)",
84
+ endpoint: "reference.overseas-account-search",
85
+ fields: [
86
+ required("--keyword <text>", "Account name fragment: substring match anywhere in the name, case-insensitive (e.g. elo matches Elon Musk and Financelot)", "accountName"),
87
+ from(), size(),
88
+ format(), output(),
89
+ ],
90
+ });
@@ -1,18 +1,19 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { Option } from "commander";
3
+ import { InvalidArgumentError, Option } from "commander";
4
4
  import { QUOTE_MAX_LIMIT } from "../core/quoteSharding.js";
5
5
  import { collectList, dateArg, datetimeArg, maybeArray, numberListArg, parseChoiceList, parseFrom, parseNumberOption, parseOptionalNumberOption, parseSize } from "../core/args.js";
6
6
  import { loadConfig } from "../core/config.js";
7
- import { placeStreamedDownload, releaseClaim, resolveTitle, saveDownloadResult, uniquePath } from "../core/download.js";
7
+ import { assertOutputFilePath, placeStreamedDownload, releaseClaim, resolveTitle, saveDownloadResult, uniquePath } from "../core/download.js";
8
+ import { isDryRun } from "../core/dryRun.js";
8
9
  import { ENDPOINTS } from "../core/endpoints.js";
9
10
  import { ValidationError } from "../core/errors.js";
10
11
  import { parseOutputFormat, stagingPath } from "../core/output.js";
11
- import { printData } from "../core/printer.js";
12
+ import { columnsSelected, printData } from "../core/printer.js";
12
13
  import { ExportSink, getRowSink } from "../core/rowSink.js";
13
14
  export async function createClient(stream) {
14
15
  const { GangtiseClient } = await import("../core/client.js");
15
- const sink = stream?.output && (stream.format === "jsonl" || stream.format === "csv") ? new ExportSink(stream.output, stream.format, stream.cache) : undefined;
16
+ const sink = stream?.output && (stream.format === "jsonl" || stream.format === "csv") && !columnsSelected() ? new ExportSink(stream.output, stream.format, stream.cache) : undefined;
16
17
  const client = new GangtiseClient(loadConfig(), sink);
17
18
  client.allowCostlyFetch = Boolean(stream?.yes);
18
19
  return client;
@@ -89,6 +90,7 @@ export function noteLateStart(data, requestedStart, field, label, cause = "The s
89
90
  }
90
91
  export async function runDownload(client, endpointKey, query, options) {
91
92
  if (options.output) {
93
+ await assertOutputFilePath(options.output);
92
94
  const result = await client.call(endpointKey, options.body, query, { streamTo: options.output });
93
95
  await saveDownloadResult(result, options.fallbackName, options.output);
94
96
  return;
@@ -120,7 +122,7 @@ export async function runDownload(client, endpointKey, query, options) {
120
122
  }
121
123
  }
122
124
  export function addDownloadCommand(parent, spec) {
123
- const cmd = parent.command(spec.name ?? "download").requiredOption(`${spec.idOption} <id>`);
125
+ const cmd = parent.command(spec.name ?? "download").requiredOption(`${spec.idOption} <id>`, `The ${spec.idField} of the record, from the matching list command`);
124
126
  if (spec.fileType) {
125
127
  const option = new Option("--file-type <number>", spec.fileType.description).choices(spec.fileType.choices);
126
128
  cmd.addOption(spec.fileType.required ? option.makeOptionMandatory() : option.default(spec.fileType.default));
@@ -132,7 +134,7 @@ export function addDownloadCommand(parent, spec) {
132
134
  if (spec.titleListEndpoint) {
133
135
  cmd.option("--resolve-title", "On a title-cache miss, query the list endpoint for a friendly filename (4 extra requests; most of these list endpoints bill per row). Without it the server filename or <prefix>-<id> is used. Ignored when --output is given");
134
136
  }
135
- cmd.option("--output <path>").action((options) => withClient(async (client) => {
137
+ cmd.option("--output <path>", DOWNLOAD_OUTPUT_HELP).action((options) => withClient(async (client) => {
136
138
  const id = options[spec.idField];
137
139
  const qp = { [spec.idField]: id };
138
140
  if (spec.fileType && options.fileType)
@@ -151,7 +153,7 @@ export function addDownloadCommand(parent, spec) {
151
153
  }
152
154
  export function assertConfirmed(endpointKey, confirmed, target) {
153
155
  const destructive = ENDPOINTS[endpointKey]?.destructive;
154
- if (!destructive || confirmed)
156
+ if (!destructive || confirmed || isDryRun())
155
157
  return;
156
158
  throw new ValidationError(`${destructive.warning} 确认要对 ${target} 执行就加上 --yes。`);
157
159
  }
@@ -201,8 +203,18 @@ export const rankType = () => numberEnum("--rank-type <number>", "Rank type: 1=c
201
203
  export const searchType = () => numberEnum("--search-type <number>", "Search type: 1=title 2=fulltext", "searchType", ["1", "2"], "1");
202
204
  export const top = (max) => field(new Option("--top <number>", `Max results (default: 10, max: ${max})`).default("10"), (v) => ({ top: parseNumberOption(v, "--top", { integer: true, min: 1, max }) }));
203
205
  export const flag = (flags, description) => field(new Option(flags, description));
206
+ export const OUTPUT_HELP = "Write the result to this file instead of stdout (a file path, not a directory)";
207
+ export const DOWNLOAD_OUTPUT_HELP = "Save the file to this path (a file path, not a directory); omit for an automatic name in the current directory";
208
+ export const SECURITY_CODE_FLAGS = "--security, --security-code <code>";
209
+ export const SECURITY_CODE_HELP = "Security code, e.g. 600519.SH";
210
+ export const oneSecurityCode = (value) => {
211
+ if (value.includes(","))
212
+ throw new InvalidArgumentError("takes one security code; run the command once per code");
213
+ return value;
214
+ };
204
215
  export const format = (fallback = "table") => field(new Option("--format <format>", "Output format").default(fallback));
205
- export const output = (description) => field(new Option("--output <path>", description));
216
+ export const output = (description = OUTPUT_HELP) => field(new Option("--output <path>", description));
217
+ export const securityCode = (description = SECURITY_CODE_HELP) => field(new Option(SECURITY_CODE_FLAGS, description).makeOptionMandatory().argParser(oneSecurityCode), (v) => ({ securityCode: v }));
206
218
  export const from = () => field(new Option("--from <number>", "Starting offset").default("0"), (v) => ({ from: parseFrom(v) }));
207
219
  export const size = (description = "Total rows to return; omit to fetch all") => field(new Option("--size <number>", description), (v) => ({ size: parseSize(v) }));
208
220
  const asTyped = (v) => v;