Skip to content

Commit 407b559

Browse files
authored
feat(help): hard-gate empty positional help text + fix 18 offenders (#1403)
Why - `opencli twitter followers --help` rendered: Arguments: user with a blank trailing column. Both humans and agents could not recover the parameter's purpose without reading source. WAWQAQ surfaced this directly: "没有说明当后面的 followers [user] [options] 如果都没填的时候,获取的是什么?" - This is metadata completeness, not stylistic taste. Failing closed is the only way to keep the help surface trustworthy as adapters land. What - src/build-manifest.ts: add `findManifestMetadataIssues()` that flags any positional with empty / whitespace-only / missing `help`. Wired into `main()` after the import-failures gate; build aborts non-zero with a per-arg report (`site/cmd positional "name" (sourceFile)`). - src/build-manifest.test.ts: cover the gate (positives + negatives, scoped strictly to positionals — named flags are intentionally out-of-scope). - 18 adapter offenders (16 required + 2 optional) get explicit help text: twitter: followers/following/list-add/list-remove/list-tweets/ search/thread reddit: search/subreddit/user/user-comments/user-posts douyin: stats/update bilibili: subtitle jike: search Optional positionals (`twitter followers/following [user]`) now document the omit semantics — fetches the currently logged-in account. - CHANGELOG: document the build gate and the offender list. Out of scope (planned follow-ups) - Semantic-quality advisory: optional positional help should also contain `default / omit / current / logged-in / required unless …` keywords. That belongs to the planned Arg metadata v2 work (`when_omitted / when_present / value_format` 3-field schema). - Named-flag `help` quality. Named flags carry the flag name itself in help, so a missing `help` is not as opaque; if we want to gate those too, do it as a separate, intentional decision. Validation - `npm run build` → 799 entries, clean. - `npm run typecheck` → clean. - `npx vitest run --project unit --project adapter` → 257 + 4 files, all green (build-manifest 13 tests, manifest gate added). - Smoke: temporarily reverted `followers.js` help to empty → build aborts with the exact `twitter/followers positional "user" (...)` line; restored, build is clean again. - `npm run check:silent-column-drop` and `check:typed-error-lint` baselines unchanged.
1 parent dc7b88d commit 407b559

20 files changed

Lines changed: 230 additions & 36 deletions

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Bug Fixes
6+
7+
* **help / build** — every positional arg must now declare a non-empty `help` string. The build-manifest step fails closed when a positional has empty / whitespace-only / missing `help`, so `opencli <site> <cmd> --help` always shows callers what each parameter is for. Pre-existing offenders (`twitter followers/following/list-add/list-remove/list-tweets/search/thread`, `reddit search/subreddit/user/user-comments/user-posts`, `douyin stats/update`, `bilibili subtitle`, `jike search`) now have explicit help text — most notably `twitter followers [user]` and `following [user]` now document that omitting the user fetches the currently logged-in account.
8+
39
## [1.7.14](https://github.com/jackwener/opencli/compare/v1.7.13...v1.7.14) (2026-05-08)
410

511
### Features

cli-manifest.json

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2443,7 +2443,7 @@
24432443
"type": "str",
24442444
"required": true,
24452445
"positional": true,
2446-
"help": ""
2446+
"help": "Bilibili 视频 BV ID(如 BV1xx411c7mD),或视频 URL / b23.tv 短链"
24472447
},
24482448
{
24492449
"name": "lang",
@@ -8491,7 +8491,7 @@
84918491
"type": "str",
84928492
"required": true,
84938493
"positional": true,
8494-
"help": ""
8494+
"help": "抖音作品 ID(aweme_id,可从作品 URL 末尾获取)"
84958495
}
84968496
],
84978497
"columns": [
@@ -8517,7 +8517,7 @@
85178517
"type": "str",
85188518
"required": true,
85198519
"positional": true,
8520-
"help": ""
8520+
"help": "抖音作品 ID(aweme_id,可从作品 URL 末尾获取)"
85218521
},
85228522
{
85238523
"name": "reschedule",
@@ -13173,7 +13173,7 @@
1317313173
"type": "string",
1317413174
"required": true,
1317513175
"positional": true,
13176-
"help": ""
13176+
"help": "即刻搜索关键词"
1317713177
},
1317813178
{
1317913179
"name": "limit",
@@ -19350,7 +19350,7 @@
1935019350
"type": "string",
1935119351
"required": true,
1935219352
"positional": true,
19353-
"help": ""
19353+
"help": "Reddit search query"
1935419354
},
1935519355
{
1935619356
"name": "subreddit",
@@ -19408,7 +19408,7 @@
1940819408
"type": "string",
1940919409
"required": true,
1941019410
"positional": true,
19411-
"help": ""
19411+
"help": "Subreddit name (no `r/` prefix; e.g. `python`)"
1941219412
},
1941319413
{
1941419414
"name": "sort",
@@ -19553,7 +19553,7 @@
1955319553
"type": "string",
1955419554
"required": true,
1955519555
"positional": true,
19556-
"help": ""
19556+
"help": "Reddit username (no `u/` prefix needed)"
1955719557
}
1955819558
],
1955919559
"columns": [
@@ -19579,7 +19579,7 @@
1957919579
"type": "string",
1958019580
"required": true,
1958119581
"positional": true,
19582-
"help": ""
19582+
"help": "Reddit username (no `u/` prefix needed)"
1958319583
},
1958419584
{
1958519585
"name": "limit",
@@ -19614,7 +19614,7 @@
1961419614
"type": "string",
1961519615
"required": true,
1961619616
"positional": true,
19617-
"help": ""
19617+
"help": "Reddit username (no `u/` prefix needed)"
1961819618
},
1961919619
{
1962019620
"name": "limit",
@@ -22375,7 +22375,7 @@
2237522375
"type": "string",
2237622376
"required": false,
2237722377
"positional": true,
22378-
"help": ""
22378+
"help": "Twitter/X handle (with or without @). Omit to fetch followers of the currently logged-in account."
2237922379
},
2238022380
{
2238122381
"name": "limit",
@@ -22409,7 +22409,7 @@
2240922409
"type": "string",
2241022410
"required": false,
2241122411
"positional": true,
22412-
"help": ""
22412+
"help": "Twitter/X handle (with or without @). Omit to fetch the accounts the currently logged-in user follows."
2241322413
},
2241422414
{
2241522415
"name": "limit",
@@ -22537,14 +22537,14 @@
2253722537
"type": "string",
2253822538
"required": true,
2253922539
"positional": true,
22540-
"help": ""
22540+
"help": "Numeric ID of the list you own (e.g. from `opencli twitter lists`)"
2254122541
},
2254222542
{
2254322543
"name": "username",
2254422544
"type": "string",
2254522545
"required": true,
2254622546
"positional": true,
22547-
"help": ""
22547+
"help": "Twitter/X handle to add (with or without @)"
2254822548
}
2254922549
],
2255022550
"columns": [
@@ -22573,14 +22573,14 @@
2257322573
"type": "string",
2257422574
"required": true,
2257522575
"positional": true,
22576-
"help": ""
22576+
"help": "Numeric ID of the list you own (e.g. from `opencli twitter lists`)"
2257722577
},
2257822578
{
2257922579
"name": "username",
2258022580
"type": "string",
2258122581
"required": true,
2258222582
"positional": true,
22583-
"help": ""
22583+
"help": "Twitter/X handle to remove (with or without @)"
2258422584
}
2258522585
],
2258622586
"columns": [
@@ -22609,7 +22609,7 @@
2260922609
"type": "string",
2261022610
"required": true,
2261122611
"positional": true,
22612-
"help": ""
22612+
"help": "Numeric ID of a Twitter/X list (e.g. from `opencli twitter lists`)"
2261322613
},
2261422614
{
2261522615
"name": "limit",
@@ -22929,7 +22929,7 @@
2292922929
"type": "string",
2293022930
"required": true,
2293122931
"positional": true,
22932-
"help": ""
22932+
"help": "Twitter/X search query (operators like `from:` and `since:` are supported)"
2293322933
},
2293422934
{
2293522935
"name": "filter",
@@ -22980,7 +22980,7 @@
2298022980
"type": "string",
2298122981
"required": true,
2298222982
"positional": true,
22983-
"help": ""
22983+
"help": "Tweet numeric ID (e.g. 1234567890) or full status URL"
2298422984
},
2298522985
{
2298622986
"name": "limit",

clis/bilibili/subtitle.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ cli({
88
description: '获取 Bilibili 视频的字幕',
99
strategy: Strategy.COOKIE,
1010
args: [
11-
{ name: 'bvid', required: true, positional: true },
11+
{ name: 'bvid', required: true, positional: true, help: 'Bilibili 视频 BV ID(如 BV1xx411c7mD),或视频 URL / b23.tv 短链' },
1212
{ name: 'lang', required: false, help: '字幕语言代码 (如 zh-CN, en-US, ai-zh),默认取第一个' },
1313
],
1414
columns: ['index', 'from', 'to', 'content'],

clis/douyin/stats.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ cli({
88
domain: 'creator.douyin.com',
99
strategy: Strategy.COOKIE,
1010
args: [
11-
{ name: 'aweme_id', required: true, positional: true },
11+
{ name: 'aweme_id', required: true, positional: true, help: '抖音作品 ID(aweme_id,可从作品 URL 末尾获取)' },
1212
],
1313
columns: ['metric', 'value'],
1414
func: async (page, kwargs) => {

clis/douyin/update.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ cli({
1010
domain: 'creator.douyin.com',
1111
strategy: Strategy.COOKIE,
1212
args: [
13-
{ name: 'aweme_id', required: true, positional: true },
13+
{ name: 'aweme_id', required: true, positional: true, help: '抖音作品 ID(aweme_id,可从作品 URL 末尾获取)' },
1414
{ name: 'reschedule', default: '', help: '新的发布时间(ISO8601 或 Unix 秒)' },
1515
{ name: 'caption', default: '', help: '新的正文内容' },
1616
],

clis/jike/search.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ cli({
1515
strategy: Strategy.COOKIE,
1616
browser: true,
1717
args: [
18-
{ name: 'query', type: 'string', required: true, positional: true },
18+
{ name: 'query', type: 'string', required: true, positional: true, help: '即刻搜索关键词' },
1919
{ name: 'limit', type: 'int', default: 20 },
2020
],
2121
columns: ['id', 'author', 'content', 'likes', 'comments', 'time', 'url'],

clis/reddit/search.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ cli({
88
strategy: Strategy.COOKIE,
99
browser: true,
1010
args: [
11-
{ name: 'query', type: 'string', required: true, positional: true },
11+
{ name: 'query', type: 'string', required: true, positional: true, help: 'Reddit search query' },
1212
{
1313
name: 'subreddit',
1414
type: 'string',

clis/reddit/subreddit.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ cli({
88
strategy: Strategy.COOKIE,
99
browser: true,
1010
args: [
11-
{ name: 'name', type: 'string', required: true, positional: true },
11+
{ name: 'name', type: 'string', required: true, positional: true, help: 'Subreddit name (no `r/` prefix; e.g. `python`)' },
1212
{
1313
name: 'sort',
1414
type: 'string',

clis/reddit/user-comments.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ cli({
88
strategy: Strategy.COOKIE,
99
browser: true,
1010
args: [
11-
{ name: 'username', type: 'string', required: true, positional: true },
11+
{ name: 'username', type: 'string', required: true, positional: true, help: 'Reddit username (no `u/` prefix needed)' },
1212
{ name: 'limit', type: 'int', default: 15 },
1313
],
1414
columns: ['subreddit', 'score', 'body', 'url'],

clis/reddit/user-posts.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ cli({
88
strategy: Strategy.COOKIE,
99
browser: true,
1010
args: [
11-
{ name: 'username', type: 'string', required: true, positional: true },
11+
{ name: 'username', type: 'string', required: true, positional: true, help: 'Reddit username (no `u/` prefix needed)' },
1212
{ name: 'limit', type: 'int', default: 15 },
1313
],
1414
columns: ['title', 'subreddit', 'score', 'comments', 'url'],

0 commit comments

Comments
 (0)