chore: import upstream snapshot with attribution
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
This commit is contained in:
Executable
+81
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
# check-doc-coverage.sh — Verify every adapter in clis/ has a doc page.
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 — all adapters have docs
|
||||
# 1 — at least one adapter is missing documentation
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/check-doc-coverage.sh # report only
|
||||
# bash scripts/check-doc-coverage.sh --strict # exit 1 on missing docs
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
STRICT=false
|
||||
if [[ "${1:-}" == "--strict" ]]; then
|
||||
STRICT=true
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
SRC_DIR="$ROOT_DIR/clis"
|
||||
DOCS_DIR="$ROOT_DIR/docs/adapters"
|
||||
|
||||
missing=()
|
||||
covered=0
|
||||
total=0
|
||||
|
||||
for adapter_dir in "$SRC_DIR"/*/; do
|
||||
adapter_name="$(basename "$adapter_dir")"
|
||||
# Skip internal directories (e.g., _shared)
|
||||
[[ "$adapter_name" == _* ]] && continue
|
||||
# Skip directories that only contain utility files (prefixed with _)
|
||||
has_commands=false
|
||||
for f in "$adapter_dir"*; do
|
||||
fname="$(basename "$f")"
|
||||
[[ "$fname" == _* ]] && continue
|
||||
[[ "$fname" == *.test.* ]] && continue
|
||||
has_commands=true
|
||||
break
|
||||
done
|
||||
[[ "$has_commands" == false ]] && continue
|
||||
total=$((total + 1))
|
||||
|
||||
# Check if doc exists in browser/ or desktop/ subdirectories
|
||||
if [[ -f "$DOCS_DIR/browser/$adapter_name.md" ]] || \
|
||||
[[ -f "$DOCS_DIR/desktop/$adapter_name.md" ]]; then
|
||||
covered=$((covered + 1))
|
||||
else
|
||||
# Handle directory name mismatches (e.g., discord-app -> discord)
|
||||
alt_name="${adapter_name%-app}"
|
||||
if [[ "$alt_name" != "$adapter_name" ]] && \
|
||||
{ [[ -f "$DOCS_DIR/browser/$alt_name.md" ]] || \
|
||||
[[ -f "$DOCS_DIR/desktop/$alt_name.md" ]]; }; then
|
||||
covered=$((covered + 1))
|
||||
else
|
||||
missing+=("$adapter_name")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo "📊 Doc Coverage: $covered/$total adapters documented"
|
||||
echo ""
|
||||
|
||||
if [[ ${#missing[@]} -gt 0 ]]; then
|
||||
echo "⚠️ Missing docs for ${#missing[@]} adapter(s):"
|
||||
for name in "${missing[@]}"; do
|
||||
echo " - $name → create docs/adapters/browser/$name.md or docs/adapters/desktop/$name.md"
|
||||
done
|
||||
echo ""
|
||||
if $STRICT; then
|
||||
echo "❌ Doc check failed (--strict mode)."
|
||||
exit 1
|
||||
else
|
||||
echo "💡 Run with --strict to fail CI on missing docs."
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
echo "✅ All adapters have documentation."
|
||||
exit 0
|
||||
fi
|
||||
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-listing-id-pairing.mjs — advisory report on listing↔detail id round-tripping.
|
||||
*
|
||||
* Soft convention (NOT a CI gate): when a site exposes both a listing-class
|
||||
* command (search / hot / recent / trending / top / feed / popular / new /
|
||||
* list) AND a detail-class command (read / article / paper / post / detail /
|
||||
* view / job / page / book / movie / show / chapter / question / answer /
|
||||
* tweet / video / track), it's usually nicer for agents if every listing row
|
||||
* carries an id-shaped column whose value round-trips into the detail
|
||||
* command. Without that, the agent has to re-search by title or scrape a URL
|
||||
* to follow up.
|
||||
*
|
||||
* Why advisory and not a gate: whether a listing should pair with a detail
|
||||
* is a case-by-case product/UX call (topic-string trending, profile-attribute
|
||||
* key/value rows, UI-only sessions etc. legitimately don't pair). Forcing
|
||||
* authors through an exempt list every PR was higher cognitive cost than the
|
||||
* silent-loss bugs the rule actually catches. See PR #1311 thread for the
|
||||
* "anti-pattern vs case-by-case" filter.
|
||||
*
|
||||
* What this script does:
|
||||
* 1. Group cli-manifest.json entries by site.
|
||||
* 2. For each site that has both classes, walk every listing entry and
|
||||
* check `columns` for at least one id-shaped name.
|
||||
* 3. Print a report. Always exits 0 — never fails CI.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/check-listing-id-pairing.mjs # print advisory report
|
||||
* npm run advise:listing-id-pairing
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const MANIFEST = resolve(__dirname, '..', 'cli-manifest.json');
|
||||
|
||||
/**
|
||||
* Listing-class commands. Each row represents a single fetchable entity
|
||||
* (post / paper / job / ...). The id of that entity must round-trip into
|
||||
* the site's detail command.
|
||||
*/
|
||||
const LISTING_NAMES = new Set([
|
||||
'search', 'hot', 'recent', 'trending', 'top', 'feed', 'popular',
|
||||
'list', 'best', 'newest', 'latest', 'rising', 'controversial',
|
||||
'home', 'timeline', 'browse', 'discover', 'jobs',
|
||||
'unanswered', 'bounties', 'tag', 'user', 'venue',
|
||||
'category', 'subreddit', 'question',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Listing-class commands whose rows are sub-resources within a parent
|
||||
* thread/session, NOT independently fetchable. Excluded from the rule:
|
||||
*
|
||||
* - `comments` / `replies` / `reviews` / `answer-list` / `thread-list`
|
||||
* — rows are comments under a parent post; the detail command fetches
|
||||
* the parent, not the comment
|
||||
* - `ask` / `new` / `show` for AI-chat / agent-session sites — rows are
|
||||
* conversation turns within one session, not separately addressable
|
||||
*
|
||||
* These are intentionally NOT in `LISTING_NAMES` so the rule doesn't
|
||||
* fire on them.
|
||||
*/
|
||||
|
||||
|
||||
const DETAIL_NAMES = new Set([
|
||||
'read', 'article', 'paper', 'post', 'detail', 'view', 'job',
|
||||
'page', 'book', 'movie', 'show-detail', 'chapter', 'tweet',
|
||||
'video', 'track', 'note', 'review', 'item', 'product', 'episode',
|
||||
'thread', 'comment-detail', 'profile-detail', 'shop',
|
||||
]);
|
||||
|
||||
/** Columns whose name implies "this is an id you can pass to detail". */
|
||||
const ID_COLUMN_PATTERNS = [
|
||||
/^id$/i,
|
||||
/_id$/i,
|
||||
/Id$/,
|
||||
/^short_id$/i,
|
||||
/^jk$/i, // indeed
|
||||
/^tid$/i, // hupu / thread id
|
||||
/^bvid$/i, // bilibili
|
||||
/^aid$/i, // anime / bilibili av
|
||||
/^asin$/i, // amazon
|
||||
/^sku$/i, // jd / retail product SKU
|
||||
/^isbn$/i, // book sites
|
||||
/^doi$/i, // arxiv / openreview
|
||||
/^slug$/i, // dev.to / lobsters short slug
|
||||
/^hn_id$/i,
|
||||
/^username$/i, // user-keyed detail (profile commands)
|
||||
/^handle$/i,
|
||||
/^uri$/i, // bluesky AT URI (at://did:.../...)
|
||||
];
|
||||
|
||||
function isUrlDetailCommand(entry) {
|
||||
const args = Array.isArray(entry.args) ? entry.args : [];
|
||||
const primaryArg = args.find((arg) => arg?.positional || arg?.required) ?? args[0];
|
||||
if (!primaryArg) return false;
|
||||
const name = String(primaryArg.name ?? '').toLowerCase();
|
||||
if (name === 'url' || name === 'url-or-id') return true;
|
||||
|
||||
const help = String(primaryArg.help ?? '').toLowerCase();
|
||||
if (!help) return false;
|
||||
|
||||
// Accept only explicit "this argument may be a URL" wording. Phrases
|
||||
// like "id from URL" mean callers must extract an id before invoking
|
||||
// the detail command, so listing.url must not satisfy the id-pair gate.
|
||||
return (
|
||||
/^full\b[^()]*\burl\b/.test(help) ||
|
||||
/\burl\s+or\s+[^()]*\bid\b/.test(help) ||
|
||||
/\bor\s+(?:a\s+)?full\b[^()]*\burl\b/.test(help) ||
|
||||
/\bor\s+url\b/.test(help) ||
|
||||
/\burl\s*,\s*or\b/.test(help)
|
||||
);
|
||||
}
|
||||
|
||||
function isIdColumn(col, detailCommands) {
|
||||
if (ID_COLUMN_PATTERNS.some((re) => re.test(col))) return true;
|
||||
if (/^url$/i.test(col)) {
|
||||
return detailCommands.some(isUrlDetailCommand);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function classify(name) {
|
||||
if (LISTING_NAMES.has(name)) return 'listing';
|
||||
if (DETAIL_NAMES.has(name)) return 'detail';
|
||||
return 'other';
|
||||
}
|
||||
|
||||
function main() {
|
||||
const manifest = JSON.parse(readFileSync(MANIFEST, 'utf8'));
|
||||
|
||||
const bySite = new Map();
|
||||
for (const entry of manifest) {
|
||||
if (!entry?.site || !entry?.name) continue;
|
||||
if (!bySite.has(entry.site)) bySite.set(entry.site, []);
|
||||
bySite.get(entry.site).push(entry);
|
||||
}
|
||||
|
||||
const findings = [];
|
||||
let scannedSites = 0;
|
||||
let scannedListings = 0;
|
||||
|
||||
for (const [site, entries] of bySite) {
|
||||
// Only `access: 'read'` detail commands count — write commands like
|
||||
// `instagram/post` or `instagram/note` create remote state, they don't
|
||||
// fetch by id, so the listing→detail pairing rule doesn't apply.
|
||||
const readDetail = entries.filter(
|
||||
(e) => classify(e.name) === 'detail' && e.access === 'read',
|
||||
);
|
||||
const hasListing = entries.some((e) => classify(e.name) === 'listing');
|
||||
if (!hasListing || readDetail.length === 0) continue;
|
||||
scannedSites++;
|
||||
|
||||
for (const entry of entries) {
|
||||
if (classify(entry.name) !== 'listing') continue;
|
||||
scannedListings++;
|
||||
const columns = Array.isArray(entry.columns) ? entry.columns : [];
|
||||
if (!columns.some((col) => isIdColumn(col, readDetail))) {
|
||||
findings.push({
|
||||
site,
|
||||
name: entry.name,
|
||||
columns,
|
||||
detail: readDetail.map((e) => e.name),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Scanned ${scannedSites} site(s) with both listing and read-detail commands.`);
|
||||
console.log(`Checked ${scannedListings} listing command(s).`);
|
||||
|
||||
if (findings.length === 0) {
|
||||
console.log('OK — every listing carries an id-shaped column.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log(`Advisory: ${findings.length} listing(s) without a round-trippable id column.`);
|
||||
console.log('Some of these are legitimate (topic strings, profile-attribute rows, UI-only');
|
||||
console.log('sessions); others may be worth adding an id to. Use judgment, not a gate.');
|
||||
console.log('');
|
||||
for (const v of findings) {
|
||||
console.log(` • ${v.site}/${v.name}`);
|
||||
console.log(` columns: [${v.columns.join(', ')}]`);
|
||||
console.log(` detail commands on this site: ${v.detail.join(', ')}`);
|
||||
}
|
||||
console.log('');
|
||||
console.log('See docs/conventions/listing-detail-id-pairing.md for context and patterns.');
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-silent-column-drop.mjs — CI gate for newly introduced table-output loss.
|
||||
*
|
||||
* This gate intentionally uses a baseline. The repo currently has known
|
||||
* silent-column-drop findings, and blocking every existing violation would make
|
||||
* the first CI adoption PR too large. The invariant enforced here is:
|
||||
*
|
||||
* no new silent-column-drop signatures beyond scripts/silent-column-drop-baseline.json
|
||||
*
|
||||
* When a follow-up sweep fixes existing violations, run:
|
||||
*
|
||||
* node scripts/check-silent-column-drop.mjs --update-baseline
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const PROJECT_ROOT = resolve(__dirname, '..');
|
||||
const DIST_AUDIT = resolve(PROJECT_ROOT, 'dist', 'src', 'convention-audit.js');
|
||||
const BASELINE_PATH = resolve(__dirname, 'silent-column-drop-baseline.json');
|
||||
const UPDATE = process.argv.includes('--update-baseline');
|
||||
|
||||
if (!existsSync(DIST_AUDIT)) {
|
||||
console.error('dist/src/convention-audit.js not found. Run npm run build before this check.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { runConventionAudit } = await import(pathToFileURL(DIST_AUDIT).href);
|
||||
const report = runConventionAudit({ projectRoot: PROJECT_ROOT });
|
||||
const category = report.categories.find((item) => item.rule === 'silent-column-drop');
|
||||
const current = sortRecords((category?.violations ?? []).map(toBaselineRecord));
|
||||
|
||||
if (UPDATE) {
|
||||
writeFileSync(BASELINE_PATH, `${JSON.stringify(current, null, 2)}\n`);
|
||||
console.log(`Updated ${relative(BASELINE_PATH)} with ${current.length} silent-column-drop baseline entr${current.length === 1 ? 'y' : 'ies'}.`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!existsSync(BASELINE_PATH)) {
|
||||
console.error(`${relative(BASELINE_PATH)} not found. Run node scripts/check-silent-column-drop.mjs --update-baseline.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const baseline = sortRecords(JSON.parse(readFileSync(BASELINE_PATH, 'utf-8')));
|
||||
const baselineSignatures = new Set(baseline.map(signature));
|
||||
const currentSignatures = new Set(current.map(signature));
|
||||
const added = current.filter((record) => !baselineSignatures.has(signature(record)));
|
||||
const resolved = baseline.filter((record) => !currentSignatures.has(signature(record)));
|
||||
|
||||
console.log(`Silent-column-drop gate: current=${current.length}, baseline=${baseline.length}, new=${added.length}, resolved=${resolved.length}`);
|
||||
|
||||
if (resolved.length > 0) {
|
||||
console.log('');
|
||||
console.log('Resolved baseline entries detected. Consider shrinking the baseline:');
|
||||
for (const record of resolved) {
|
||||
console.log(` - ${record.command} ${record.file} missing=[${record.missing.join(', ')}]`);
|
||||
}
|
||||
}
|
||||
|
||||
if (added.length === 0) {
|
||||
console.log('OK - no new silent-column-drop violations.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log('New silent-column-drop violations:');
|
||||
for (const record of added) {
|
||||
console.log(` - ${record.command} ${record.file} missing=[${record.missing.join(', ')}]`);
|
||||
}
|
||||
console.log('');
|
||||
console.log('Fix the adapter columns, or if this is an intentional baseline adoption, run:');
|
||||
console.log(' node scripts/check-silent-column-drop.mjs --update-baseline');
|
||||
process.exit(1);
|
||||
|
||||
function toBaselineRecord(violation) {
|
||||
const missing = Array.isArray(violation.details?.missing)
|
||||
? violation.details.missing.map(String).sort()
|
||||
: [];
|
||||
return {
|
||||
command: String(violation.command ?? ''),
|
||||
file: String(violation.file ?? ''),
|
||||
missing,
|
||||
};
|
||||
}
|
||||
|
||||
function signature(record) {
|
||||
return `${record.command}\0${record.file}\0${record.missing.join('\0')}`;
|
||||
}
|
||||
|
||||
function sortRecords(records) {
|
||||
return records
|
||||
.map((record) => ({
|
||||
command: String(record.command),
|
||||
file: String(record.file),
|
||||
missing: Array.isArray(record.missing) ? record.missing.map(String).sort() : [],
|
||||
}))
|
||||
.sort((a, b) => signature(a).localeCompare(signature(b)));
|
||||
}
|
||||
|
||||
function relative(file) {
|
||||
return file.replace(`${PROJECT_ROOT}/`, '');
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-typed-error-lint.mjs — CI gate for newly introduced silent failures.
|
||||
*
|
||||
* Baseline mode keeps adoption small: existing findings are recorded in
|
||||
* scripts/typed-error-lint-baseline.json, while CI rejects any new
|
||||
* silent-clamp / silent-empty-fallback / silent-sentinel signatures.
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const PROJECT_ROOT = resolve(__dirname, '..');
|
||||
const DIST_AUDIT = resolve(PROJECT_ROOT, 'dist', 'src', 'convention-audit.js');
|
||||
const BASELINE_PATH = resolve(__dirname, 'typed-error-lint-baseline.json');
|
||||
const UPDATE = process.argv.includes('--update-baseline');
|
||||
const RULES = new Set(['silent-clamp', 'silent-empty-fallback', 'silent-sentinel']);
|
||||
|
||||
if (!existsSync(DIST_AUDIT)) {
|
||||
console.error('dist/src/convention-audit.js not found. Run npm run build before this check.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { runConventionAudit } = await import(pathToFileURL(DIST_AUDIT).href);
|
||||
const report = runConventionAudit({ projectRoot: PROJECT_ROOT });
|
||||
const current = addOccurrenceIndexes(sortRecords(report.categories
|
||||
.filter((category) => RULES.has(category.rule))
|
||||
.flatMap((category) => category.violations.map(toBaselineRecord))));
|
||||
|
||||
if (UPDATE) {
|
||||
writeFileSync(BASELINE_PATH, `${JSON.stringify(current, null, 2)}\n`);
|
||||
console.log(`Updated ${relative(BASELINE_PATH)} with ${current.length} typed-error lint baseline entr${current.length === 1 ? 'y' : 'ies'}.`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!existsSync(BASELINE_PATH)) {
|
||||
console.error(`${relative(BASELINE_PATH)} not found. Run node scripts/check-typed-error-lint.mjs --update-baseline.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const baseline = addOccurrenceIndexes(sortRecords(JSON.parse(readFileSync(BASELINE_PATH, 'utf-8'))));
|
||||
const baselineSignatures = new Set(baseline.map(signature));
|
||||
const currentSignatures = new Set(current.map(signature));
|
||||
const added = current.filter((record) => !baselineSignatures.has(signature(record)));
|
||||
const resolved = baseline.filter((record) => !currentSignatures.has(signature(record)));
|
||||
|
||||
console.log(`Typed-error lint gate: current=${current.length}, baseline=${baseline.length}, new=${added.length}, resolved=${resolved.length}`);
|
||||
|
||||
if (resolved.length > 0) {
|
||||
console.log('');
|
||||
console.log('Resolved baseline entries detected. Consider shrinking the baseline:');
|
||||
for (const record of resolved) {
|
||||
console.log(` - ${record.rule} ${record.command} ${record.file}:${record.line}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (added.length === 0) {
|
||||
console.log('OK - no new typed-error lint violations.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log('New typed-error lint violations:');
|
||||
for (const record of added) {
|
||||
console.log(` - ${record.rule} ${record.command} ${record.file}:${record.line}`);
|
||||
if (record.text) console.log(` ${record.text}`);
|
||||
}
|
||||
console.log('');
|
||||
console.log('Fix the silent fallback, or if this is an intentional baseline adoption, run:');
|
||||
console.log(' node scripts/check-typed-error-lint.mjs --update-baseline');
|
||||
process.exit(1);
|
||||
|
||||
function toBaselineRecord(violation) {
|
||||
return {
|
||||
rule: String(violation.rule ?? ''),
|
||||
command: String(violation.command ?? ''),
|
||||
file: String(violation.file ?? ''),
|
||||
line: Number(violation.line ?? 0),
|
||||
text: String(violation.details?.text ?? ''),
|
||||
};
|
||||
}
|
||||
|
||||
function signature(record) {
|
||||
return `${record.rule}\0${record.command}\0${record.file}\0${record.text}\0${record.occurrence}`;
|
||||
}
|
||||
|
||||
function sortRecords(records) {
|
||||
return records
|
||||
.map((record) => ({
|
||||
rule: String(record.rule),
|
||||
command: String(record.command),
|
||||
file: String(record.file),
|
||||
line: Number(record.line ?? 0),
|
||||
text: String(record.text ?? ''),
|
||||
occurrence: Number(record.occurrence ?? 0),
|
||||
}))
|
||||
.sort((a, b) => stableOrder(a).localeCompare(stableOrder(b)));
|
||||
}
|
||||
|
||||
function addOccurrenceIndexes(records) {
|
||||
const seen = new Map();
|
||||
return records.map((record) => {
|
||||
const key = `${record.rule}\0${record.command}\0${record.file}\0${record.text}`;
|
||||
const occurrence = seen.get(key) ?? 0;
|
||||
seen.set(key, occurrence + 1);
|
||||
return { ...record, occurrence };
|
||||
});
|
||||
}
|
||||
|
||||
function stableOrder(record) {
|
||||
return `${record.rule}\0${record.command}\0${record.file}\0${record.text}\0${record.line}`;
|
||||
}
|
||||
|
||||
function relative(file) {
|
||||
return file.replace(`${PROJECT_ROOT}/`, '');
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Remove dist/ before a fresh build so deleted source modules do not leave
|
||||
* stale compiled files behind.
|
||||
*/
|
||||
const { existsSync, rmSync } = require('fs');
|
||||
|
||||
if (existsSync('dist')) {
|
||||
rmSync('dist', { recursive: true, force: true });
|
||||
}
|
||||
|
||||
if (existsSync('tsconfig.tsbuildinfo')) {
|
||||
rmSync('tsconfig.tsbuildinfo', { force: true });
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copy YAML support files to dist/.
|
||||
* (Adapters are JS-first and no longer need yaml copying.)
|
||||
*/
|
||||
const { copyFileSync, mkdirSync, existsSync } = require('fs');
|
||||
|
||||
// Copy external CLI registry to dist/
|
||||
const extSrc = 'src/external-clis.yaml';
|
||||
if (existsSync(extSrc)) {
|
||||
mkdirSync('dist/src', { recursive: true });
|
||||
copyFileSync(extSrc, 'dist/src/external-clis.yaml');
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Sparse adapter sync: keeps ~/.opencli/clis/ clean by removing stale overrides.
|
||||
*
|
||||
* Strategy (hash-based, site-level granularity):
|
||||
* - When an official site has upstream changes: DELETE the local override
|
||||
* (do NOT copy new version — runtime falls back to package baseline)
|
||||
* - When an official site has no changes: leave local override intact
|
||||
* - User-created custom sites (not in package): always preserved
|
||||
* - Skips entirely if already synced at the same version
|
||||
*
|
||||
* ~/.opencli/clis/ is a sparse override layer, not a full copy.
|
||||
* Only eject-ed or user-modified sites appear here.
|
||||
*
|
||||
* Only runs on global install (npm install -g) or explicit OPENCLI_FETCH=1.
|
||||
* No network calls — reads hashes from clis/ in the installed package.
|
||||
*
|
||||
* This is an ESM script (package.json type: module). No TypeScript, no src/ imports.
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, rmSync, readFileSync, writeFileSync, readdirSync, statSync, unlinkSync } from 'node:fs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { join, resolve, dirname, relative } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
const OPENCLI_DIR = join(homedir(), '.opencli');
|
||||
const USER_CLIS_DIR = join(OPENCLI_DIR, 'clis');
|
||||
const MANIFEST_PATH = join(OPENCLI_DIR, 'adapter-manifest.json');
|
||||
const PACKAGE_ROOT = resolve(import.meta.dirname, '..');
|
||||
const BUILTIN_CLIS = join(PACKAGE_ROOT, 'clis');
|
||||
|
||||
function log(msg) {
|
||||
console.log(`[opencli] ${msg}`);
|
||||
}
|
||||
|
||||
function getPackageVersion() {
|
||||
try {
|
||||
return JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf-8')).version;
|
||||
} catch {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute SHA-256 hash of file content.
|
||||
*/
|
||||
function fileHash(filePath) {
|
||||
return createHash('sha256').update(readFileSync(filePath)).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Read existing manifest. Returns { version, files, hashes } or null.
|
||||
*/
|
||||
function readManifest() {
|
||||
try {
|
||||
return JSON.parse(readFileSync(MANIFEST_PATH, 'utf-8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all relative file paths under a directory.
|
||||
*/
|
||||
function walkFiles(dir, prefix = '') {
|
||||
const results = [];
|
||||
if (!existsSync(dir)) return results;
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const full = join(dir, entry);
|
||||
const rel = prefix ? `${prefix}/${entry}` : entry;
|
||||
if (statSync(full).isDirectory()) {
|
||||
results.push(...walkFiles(full, rel));
|
||||
} else {
|
||||
results.push(rel);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove empty parent directories up to (but not including) stopAt.
|
||||
*/
|
||||
function pruneEmptyDirs(filePath, stopAt) {
|
||||
const boundary = resolve(stopAt);
|
||||
let dir = resolve(dirname(filePath));
|
||||
while (dir !== boundary) {
|
||||
const rel = relative(boundary, dir);
|
||||
if (!rel || rel.startsWith('..')) break;
|
||||
try {
|
||||
const entries = readdirSync(dir);
|
||||
if (entries.length > 0) break;
|
||||
rmSync(dir);
|
||||
dir = dirname(dir);
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function fetchAdapters() {
|
||||
const currentVersion = getPackageVersion();
|
||||
const oldManifest = readManifest();
|
||||
|
||||
// Skip if already installed at the same version (unless forced via OPENCLI_FETCH=1)
|
||||
const isForced = process.env.OPENCLI_FETCH === '1';
|
||||
if (!isForced && currentVersion !== 'unknown' && oldManifest?.version === currentVersion) {
|
||||
log(`Adapters already up to date (v${currentVersion})`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!existsSync(BUILTIN_CLIS)) {
|
||||
log('Warning: clis/ not found in package — skipping adapter copy');
|
||||
return;
|
||||
}
|
||||
|
||||
const newOfficialFiles = new Set(walkFiles(BUILTIN_CLIS));
|
||||
const oldOfficialFiles = new Set(oldManifest?.files ?? []);
|
||||
const rawHashes = oldManifest?.hashes;
|
||||
// Guard against corrupted manifest: if hashes is a non-object type (string, number,
|
||||
// array), skip sync to avoid false-positive "changed" detection that deletes overrides.
|
||||
// null/undefined are treated as empty (old manifests may lack the field).
|
||||
if (rawHashes != null && (typeof rawHashes !== 'object' || Array.isArray(rawHashes))) {
|
||||
log('Warning: adapter-manifest.json has corrupted hashes — skipping sync. Will fix on next run.');
|
||||
return;
|
||||
}
|
||||
const oldHashes = rawHashes ?? {};
|
||||
mkdirSync(USER_CLIS_DIR, { recursive: true });
|
||||
|
||||
// 1. Compute new hashes and detect which sites have changes
|
||||
const newHashes = {};
|
||||
const siteFiles = new Map(); // site -> [relPath, ...]
|
||||
for (const relPath of newOfficialFiles) {
|
||||
const src = join(BUILTIN_CLIS, relPath);
|
||||
const srcHash = fileHash(src);
|
||||
newHashes[relPath] = srcHash;
|
||||
|
||||
const site = relPath.split('/')[0];
|
||||
if (!siteFiles.has(site)) siteFiles.set(site, []);
|
||||
siteFiles.get(site).push(relPath);
|
||||
}
|
||||
|
||||
// Determine which sites have any changed/new/removed files
|
||||
const changedSites = new Set();
|
||||
for (const [site, files] of siteFiles) {
|
||||
for (const relPath of files) {
|
||||
if (oldHashes[relPath] !== newHashes[relPath]) {
|
||||
changedSites.add(site);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Also mark sites that had files removed
|
||||
for (const relPath of oldOfficialFiles) {
|
||||
if (!newOfficialFiles.has(relPath)) {
|
||||
changedSites.add(relPath.split('/')[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Sparse cleanup: for changed/removed official sites, delete local overrides.
|
||||
// Do NOT copy new versions — runtime falls back to package baseline.
|
||||
// Only eject-ed sites live in ~/.opencli/clis/.
|
||||
let cleared = 0;
|
||||
for (const site of changedSites) {
|
||||
const siteDir = join(USER_CLIS_DIR, site);
|
||||
if (existsSync(siteDir)) {
|
||||
rmSync(siteDir, { recursive: true, force: true });
|
||||
cleared++;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Clean up stale .ts adapter files left by older versions (pre-1.7.1)
|
||||
// Older versions shipped adapters as .ts; current versions use .js only.
|
||||
let tsCleaned = 0;
|
||||
for (const relPath of walkFiles(USER_CLIS_DIR)) {
|
||||
if (relPath.endsWith('.ts') && !relPath.endsWith('.d.ts')) {
|
||||
const jsCounterpart = relPath.replace(/\.ts$/, '.js');
|
||||
if (newOfficialFiles.has(jsCounterpart)) {
|
||||
try {
|
||||
unlinkSync(join(USER_CLIS_DIR, relPath));
|
||||
pruneEmptyDirs(join(USER_CLIS_DIR, relPath), USER_CLIS_DIR);
|
||||
tsCleaned++;
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tsCleaned > 0) log(`Cleaned up ${tsCleaned} stale .ts adapter files`);
|
||||
|
||||
// 3b. Clean up stale .yaml/.yml adapter files left by older versions (pre-1.7.0)
|
||||
// Older versions shipped adapters as YAML; current versions use .js only.
|
||||
// These are no longer discoverable and can shadow the current .js adapter layout.
|
||||
let yamlCleaned = 0;
|
||||
for (const relPath of walkFiles(USER_CLIS_DIR)) {
|
||||
if (relPath.endsWith('.yaml') || relPath.endsWith('.yml')) {
|
||||
const jsCounterpart = relPath.replace(/\.ya?ml$/, '.js');
|
||||
if (newOfficialFiles.has(jsCounterpart)) {
|
||||
try {
|
||||
unlinkSync(join(USER_CLIS_DIR, relPath));
|
||||
pruneEmptyDirs(join(USER_CLIS_DIR, relPath), USER_CLIS_DIR);
|
||||
yamlCleaned++;
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (yamlCleaned > 0) log(`Cleaned up ${yamlCleaned} stale .yaml adapter files`);
|
||||
|
||||
// 4. Clean up legacy compat shim files from ~/.opencli/
|
||||
// These were created by an older approach that placed re-export shims directly
|
||||
// in ~/.opencli/ (e.g., registry.js, errors.js, browser/). The current approach
|
||||
// uses a node_modules/@jackwener/opencli symlink instead.
|
||||
const LEGACY_SHIM_FILES = [
|
||||
'registry.js', 'errors.js', 'utils.js', 'launcher.js', 'logger.js', 'types.js',
|
||||
];
|
||||
const LEGACY_SHIM_DIRS = [
|
||||
'browser', 'download', 'errors', 'launcher', 'logger', 'pipeline', 'registry', 'types', 'utils',
|
||||
];
|
||||
let legacyCleaned = 0;
|
||||
for (const file of LEGACY_SHIM_FILES) {
|
||||
const p = join(OPENCLI_DIR, file);
|
||||
try {
|
||||
const content = readFileSync(p, 'utf-8');
|
||||
// Only delete if it's a re-export shim, not a user-created file
|
||||
if (content.includes("export * from 'file://")) {
|
||||
unlinkSync(p);
|
||||
legacyCleaned++;
|
||||
}
|
||||
} catch { /* doesn't exist */ }
|
||||
}
|
||||
for (const dir of LEGACY_SHIM_DIRS) {
|
||||
const p = join(OPENCLI_DIR, dir);
|
||||
try {
|
||||
// Delete individual shim files, then prune empty directory
|
||||
for (const entry of readdirSync(p)) {
|
||||
const fp = join(p, entry);
|
||||
try {
|
||||
if (!statSync(fp).isFile()) continue;
|
||||
const content = readFileSync(fp, 'utf-8');
|
||||
if (content.includes("export * from 'file://")) {
|
||||
unlinkSync(fp);
|
||||
legacyCleaned++;
|
||||
}
|
||||
} catch { /* skip unreadable entries */ }
|
||||
}
|
||||
// Remove directory only if now empty
|
||||
try {
|
||||
if (readdirSync(p).length === 0) rmSync(p);
|
||||
} catch { /* ignore */ }
|
||||
} catch { /* doesn't exist or not a directory */ }
|
||||
}
|
||||
|
||||
// 5. Clean up stale .plugins.lock.json.tmp-* files
|
||||
let tmpCleaned = 0;
|
||||
try {
|
||||
for (const entry of readdirSync(OPENCLI_DIR)) {
|
||||
if (entry.startsWith('.plugins.lock.json.tmp-')) {
|
||||
try {
|
||||
unlinkSync(join(OPENCLI_DIR, entry));
|
||||
tmpCleaned++;
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
if (legacyCleaned > 0 || tmpCleaned > 0) {
|
||||
log(`Cleaned up${legacyCleaned > 0 ? ` ${legacyCleaned} legacy shim files` : ''}${tmpCleaned > 0 ? `${legacyCleaned > 0 ? ',' : ''} ${tmpCleaned} stale tmp files` : ''}`);
|
||||
}
|
||||
|
||||
// 6. Write updated manifest (with per-file hashes for smart sync)
|
||||
writeFileSync(MANIFEST_PATH, JSON.stringify({
|
||||
version: currentVersion,
|
||||
files: [...newOfficialFiles].sort(),
|
||||
hashes: newHashes,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}, null, 2));
|
||||
|
||||
log(`Synced adapters: ${cleared} local override(s) cleared` +
|
||||
(tsCleaned > 0 ? `, ${tsCleaned} stale .ts files removed` : '') +
|
||||
(yamlCleaned > 0 ? `, ${yamlCleaned} stale .yaml files removed` : ''));
|
||||
}
|
||||
|
||||
function main() {
|
||||
// Skip in CI
|
||||
if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) return;
|
||||
// Only run on global install, explicit trigger, or first-run fallback
|
||||
const isGlobal = process.env.npm_config_global === 'true';
|
||||
const isExplicit = process.env.OPENCLI_FETCH === '1';
|
||||
const isFirstRun = process.env._OPENCLI_FIRST_RUN === '1';
|
||||
if (!isGlobal && !isExplicit && !isFirstRun) return;
|
||||
|
||||
fetchAdapters();
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* postinstall script — install shell completion files and print setup instructions.
|
||||
*
|
||||
* Detects the user's default shell and writes the completion script to the
|
||||
* standard completion directory. For zsh and bash, the script prints manual
|
||||
* instructions instead of modifying rc files (~/.zshrc, ~/.bashrc) — this
|
||||
* avoids breaking multi-line shell commands and other fragile rc structures.
|
||||
* Fish completions work automatically without rc changes.
|
||||
*
|
||||
* Supported shells: bash, zsh, fish.
|
||||
*
|
||||
* This script is intentionally plain Node.js (no TypeScript, no imports from
|
||||
* the main source tree) so that it can run without a build step.
|
||||
*/
|
||||
|
||||
import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
|
||||
// ── Completion script content ──────────────────────────────────────────────
|
||||
|
||||
const BASH_COMPLETION = `# Bash completion for opencli (auto-installed)
|
||||
_opencli_completions() {
|
||||
local cur words cword
|
||||
_get_comp_words_by_ref -n : cur words cword
|
||||
|
||||
local completions
|
||||
completions=$(opencli --get-completions --cursor "$cword" "\${words[@]:1}" 2>/dev/null)
|
||||
|
||||
COMPREPLY=( $(compgen -W "$completions" -- "$cur") )
|
||||
__ltrim_colon_completions "$cur"
|
||||
}
|
||||
complete -F _opencli_completions opencli
|
||||
`;
|
||||
|
||||
const ZSH_COMPLETION = `#compdef opencli
|
||||
# Zsh completion for opencli (auto-installed)
|
||||
_opencli() {
|
||||
local -a completions
|
||||
local cword=$((CURRENT - 1))
|
||||
completions=(\${(f)"$(opencli --get-completions --cursor "$cword" "\${words[@]:1}" 2>/dev/null)"})
|
||||
compadd -a completions
|
||||
}
|
||||
_opencli
|
||||
`;
|
||||
|
||||
const FISH_COMPLETION = `# Fish completion for opencli (auto-installed)
|
||||
complete -c opencli -f -a '(
|
||||
set -l tokens (commandline -cop)
|
||||
set -l cursor (count (commandline -cop))
|
||||
opencli --get-completions --cursor $cursor $tokens[2..] 2>/dev/null
|
||||
)'
|
||||
`;
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
function detectShell() {
|
||||
const shell = process.env.SHELL || '';
|
||||
if (shell.includes('zsh')) return 'zsh';
|
||||
if (shell.includes('bash')) return 'bash';
|
||||
if (shell.includes('fish')) return 'fish';
|
||||
return null;
|
||||
}
|
||||
|
||||
function ensureDir(dir) {
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
// ── Main ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function main() {
|
||||
// Skip in CI environments
|
||||
if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only install completion for global installs and npm link
|
||||
const isGlobal = process.env.npm_config_global === 'true';
|
||||
if (!isGlobal) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shell = detectShell();
|
||||
if (!shell) {
|
||||
// Cannot determine shell; silently skip
|
||||
return;
|
||||
}
|
||||
|
||||
const home = homedir();
|
||||
|
||||
try {
|
||||
switch (shell) {
|
||||
case 'zsh': {
|
||||
const completionsDir = join(home, '.zsh', 'completions');
|
||||
const completionFile = join(completionsDir, '_opencli');
|
||||
ensureDir(completionsDir);
|
||||
writeFileSync(completionFile, ZSH_COMPLETION, 'utf8');
|
||||
|
||||
console.log(`✓ Zsh completion installed to ${completionFile}`);
|
||||
console.log('');
|
||||
console.log(' \x1b[1mTo enable, add these lines to your ~/.zshrc:\x1b[0m');
|
||||
console.log(` fpath=(${completionsDir} $fpath)`);
|
||||
console.log(' autoload -Uz compinit && compinit');
|
||||
console.log('');
|
||||
console.log(' If you already have compinit (oh-my-zsh, zinit, etc.), just add the fpath line \x1b[1mbefore\x1b[0m it.');
|
||||
console.log(' Then restart your shell or run: \x1b[36mexec zsh\x1b[0m');
|
||||
break;
|
||||
}
|
||||
case 'bash': {
|
||||
const userCompDir = join(home, '.bash_completion.d');
|
||||
const completionFile = join(userCompDir, 'opencli');
|
||||
ensureDir(userCompDir);
|
||||
writeFileSync(completionFile, BASH_COMPLETION, 'utf8');
|
||||
|
||||
console.log(`✓ Bash completion installed to ${completionFile}`);
|
||||
console.log('');
|
||||
console.log(' \x1b[1mTo enable, add this line to your ~/.bashrc:\x1b[0m');
|
||||
console.log(` [ -f "${completionFile}" ] && source "${completionFile}"`);
|
||||
console.log('');
|
||||
console.log(' Then restart your shell or run: \x1b[36msource ~/.bashrc\x1b[0m');
|
||||
break;
|
||||
}
|
||||
case 'fish': {
|
||||
const completionsDir = join(home, '.config', 'fish', 'completions');
|
||||
const completionFile = join(completionsDir, 'opencli.fish');
|
||||
ensureDir(completionsDir);
|
||||
writeFileSync(completionFile, FISH_COMPLETION, 'utf8');
|
||||
|
||||
console.log(`✓ Fish completion installed to ${completionFile}`);
|
||||
console.log(` Restart your shell to activate.`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Completion install is best-effort; never fail the package install
|
||||
if (process.env.OPENCLI_VERBOSE) {
|
||||
console.error(`Warning: Could not install shell completion: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Spotify credentials template ────────────────────────────────────
|
||||
const opencliDir = join(home, '.opencli');
|
||||
const spotifyEnvFile = join(opencliDir, 'spotify.env');
|
||||
ensureDir(opencliDir);
|
||||
if (!existsSync(spotifyEnvFile)) {
|
||||
writeFileSync(spotifyEnvFile,
|
||||
`# Spotify credentials — get them at https://developer.spotify.com/dashboard\n` +
|
||||
`# Add http://127.0.0.1:8888/callback as a Redirect URI in your Spotify app\n` +
|
||||
`SPOTIFY_CLIENT_ID=your_spotify_client_id_here\n` +
|
||||
`SPOTIFY_CLIENT_SECRET=your_spotify_client_secret_here\n`,
|
||||
'utf8'
|
||||
);
|
||||
console.log(`✓ Spotify credentials template created at ${spotifyEnvFile}`);
|
||||
console.log(` Edit the file and add your Client ID and Secret, then run: opencli spotify auth`);
|
||||
}
|
||||
|
||||
// ── Browser Bridge setup hint ───────────────────────────────────────
|
||||
console.log('');
|
||||
console.log(' \x1b[1mNext step — Browser Bridge setup\x1b[0m');
|
||||
console.log(' Browser commands (bilibili, zhihu, twitter...) require the extension:');
|
||||
console.log(' 1. Download: https://github.com/jackwener/opencli/releases');
|
||||
console.log(' 2. In Chrome or Chromium, open chrome://extensions → enable Developer Mode → Load unpacked');
|
||||
console.log('');
|
||||
console.log(' Then run \x1b[36mopencli doctor\x1b[0m to verify.');
|
||||
console.log('');
|
||||
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,910 @@
|
||||
[
|
||||
{
|
||||
"command": "1688/assets",
|
||||
"file": "clis/1688/assets.js",
|
||||
"missing": [
|
||||
"detail_images",
|
||||
"item_url",
|
||||
"main_images",
|
||||
"other_images",
|
||||
"raw_assets",
|
||||
"sku_images",
|
||||
"source",
|
||||
"videos"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "1688/assets",
|
||||
"file": "clis/1688/assets.js",
|
||||
"missing": [
|
||||
"gallery",
|
||||
"href",
|
||||
"offerId",
|
||||
"offerTitle",
|
||||
"scannedAssets"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "1688/download",
|
||||
"file": "clis/1688/download.js",
|
||||
"missing": [
|
||||
"filename",
|
||||
"url"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "1688/item",
|
||||
"file": "clis/1688/item.js",
|
||||
"missing": [
|
||||
"bodyText",
|
||||
"gallery",
|
||||
"href",
|
||||
"offerId",
|
||||
"offerTitle",
|
||||
"seller",
|
||||
"services",
|
||||
"shipping",
|
||||
"trade"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "1688/item",
|
||||
"file": "clis/1688/item.js",
|
||||
"missing": [
|
||||
"currency",
|
||||
"customization_text",
|
||||
"delivery_days_text",
|
||||
"item_url",
|
||||
"main_images",
|
||||
"member_id",
|
||||
"moq_value",
|
||||
"price_tiers",
|
||||
"private_label_text",
|
||||
"sales_text",
|
||||
"seller_url",
|
||||
"service_badges",
|
||||
"shop_id",
|
||||
"shop_name",
|
||||
"stock_quantity",
|
||||
"visible_attributes"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "1688/search",
|
||||
"file": "clis/1688/search.js",
|
||||
"missing": [
|
||||
"badges",
|
||||
"currency",
|
||||
"fetched_at",
|
||||
"moq_value",
|
||||
"price_max",
|
||||
"price_min",
|
||||
"return_rate_text",
|
||||
"sales_text",
|
||||
"seller_url",
|
||||
"shop_id",
|
||||
"source_url",
|
||||
"strategy"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "1688/search",
|
||||
"file": "clis/1688/search.js",
|
||||
"missing": [
|
||||
"bodyText",
|
||||
"candidates",
|
||||
"href",
|
||||
"next_url"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "1688/search",
|
||||
"file": "clis/1688/search.js",
|
||||
"missing": [
|
||||
"container_text",
|
||||
"desc_rows",
|
||||
"hover_items",
|
||||
"hover_price_text",
|
||||
"sales_text",
|
||||
"seller_url",
|
||||
"tag_items"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "1688/store",
|
||||
"file": "clis/1688/store.js",
|
||||
"missing": [
|
||||
"business_model_text",
|
||||
"company_name",
|
||||
"company_url",
|
||||
"factory_badges",
|
||||
"member_id",
|
||||
"mobile_text",
|
||||
"phone_text",
|
||||
"response_rate_text",
|
||||
"service_badges",
|
||||
"shop_id",
|
||||
"staff_size_text",
|
||||
"store_url",
|
||||
"top_categories"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "51job/company",
|
||||
"file": "clis/51job/company.js",
|
||||
"missing": [
|
||||
"cInfoParts",
|
||||
"links",
|
||||
"sidebarText"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "51job/detail",
|
||||
"file": "clis/51job/detail.js",
|
||||
"missing": [
|
||||
"companyTag",
|
||||
"finalUrl",
|
||||
"meta"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "amazon/discussion",
|
||||
"file": "clis/amazon/discussion.js",
|
||||
"missing": [
|
||||
"average_rating_text",
|
||||
"discussion_url",
|
||||
"product_url",
|
||||
"qa_urls",
|
||||
"review_samples",
|
||||
"total_review_count_text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "amazon/offer",
|
||||
"file": "clis/amazon/offer.js",
|
||||
"missing": [
|
||||
"buybox_text",
|
||||
"href",
|
||||
"merchant_info",
|
||||
"offer_link",
|
||||
"qa_url",
|
||||
"review_url",
|
||||
"ships_from_text",
|
||||
"title"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "amazon/offer",
|
||||
"file": "clis/amazon/offer.js",
|
||||
"missing": [
|
||||
"currency",
|
||||
"merchant_info_text",
|
||||
"offer_listing_url",
|
||||
"price_value",
|
||||
"product_url",
|
||||
"qa_url",
|
||||
"review_url"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "amazon/product",
|
||||
"file": "clis/amazon/product.js",
|
||||
"missing": [
|
||||
"brand_text",
|
||||
"breadcrumbs",
|
||||
"bullet_points",
|
||||
"currency",
|
||||
"price_value",
|
||||
"product_url",
|
||||
"qa_url",
|
||||
"rating_text",
|
||||
"review_count_text",
|
||||
"review_url"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "amazon/product",
|
||||
"file": "clis/amazon/product.js",
|
||||
"missing": [
|
||||
"breadcrumbs",
|
||||
"bullets",
|
||||
"byline",
|
||||
"href",
|
||||
"product_title",
|
||||
"qa_url",
|
||||
"rating_text",
|
||||
"review_count_text",
|
||||
"review_url"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "amazon/search",
|
||||
"file": "clis/amazon/search.js",
|
||||
"missing": [
|
||||
"badge_texts",
|
||||
"href",
|
||||
"rating_text",
|
||||
"review_count_text",
|
||||
"sponsored"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "amazon/search",
|
||||
"file": "clis/amazon/search.js",
|
||||
"missing": [
|
||||
"badges",
|
||||
"currency",
|
||||
"is_sponsored",
|
||||
"price_value",
|
||||
"product_url",
|
||||
"rating_text",
|
||||
"review_count_text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "band/post",
|
||||
"file": "clis/band/post.js",
|
||||
"missing": [
|
||||
"comments",
|
||||
"photos"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "band/post",
|
||||
"file": "clis/band/post.js",
|
||||
"missing": [
|
||||
"depth"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "band/post",
|
||||
"file": "clis/band/post.js",
|
||||
"missing": [
|
||||
"filename",
|
||||
"url"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "bilibili/download",
|
||||
"file": "clis/bilibili/download.js",
|
||||
"missing": [
|
||||
"author"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "bilibili/feed",
|
||||
"file": "clis/bilibili/feed.js",
|
||||
"missing": [
|
||||
"comments",
|
||||
"itemType"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "binance/depth",
|
||||
"file": "clis/binance/depth.js",
|
||||
"missing": [
|
||||
"select"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "binance/gainers",
|
||||
"file": "clis/binance/gainers.js",
|
||||
"missing": [
|
||||
"sort_change"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "binance/losers",
|
||||
"file": "clis/binance/losers.js",
|
||||
"missing": [
|
||||
"sort_change"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "binance/ticker",
|
||||
"file": "clis/binance/ticker.js",
|
||||
"missing": [
|
||||
"sort_volume"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "binance/top",
|
||||
"file": "clis/binance/top.js",
|
||||
"missing": [
|
||||
"sort_volume"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "bloomberg/news",
|
||||
"file": "clis/bloomberg/news.js",
|
||||
"missing": [
|
||||
"errorCode",
|
||||
"message",
|
||||
"preview"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "bloomberg/news",
|
||||
"file": "clis/bloomberg/news.js",
|
||||
"missing": [
|
||||
"errorCode",
|
||||
"preview"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "boss/resume",
|
||||
"file": "clis/boss/resume.js",
|
||||
"missing": [
|
||||
"activeTime",
|
||||
"jobChatting",
|
||||
"workHistory"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "coupang/search",
|
||||
"file": "clis/coupang/search.js",
|
||||
"missing": [
|
||||
"badge",
|
||||
"category",
|
||||
"deliveryPromise",
|
||||
"deliveryType",
|
||||
"discountRate",
|
||||
"originalPrice",
|
||||
"productId",
|
||||
"reviewCount",
|
||||
"seller",
|
||||
"unitPrice"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "coupang/search",
|
||||
"file": "clis/coupang/search.js",
|
||||
"missing": [
|
||||
"originalPrice",
|
||||
"unitPrice"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "douban/download",
|
||||
"file": "clis/douban/download.js",
|
||||
"missing": [
|
||||
"detail_url",
|
||||
"image_url",
|
||||
"photo_id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "douban/marks",
|
||||
"file": "clis/douban/marks.js",
|
||||
"missing": [
|
||||
"movieId"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "douban/photos",
|
||||
"file": "clis/douban/photos.js",
|
||||
"missing": [
|
||||
"page",
|
||||
"subject_title",
|
||||
"thumb_url",
|
||||
"type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "douban/reviews",
|
||||
"file": "clis/douban/reviews.js",
|
||||
"missing": [
|
||||
"createdAt",
|
||||
"movieId",
|
||||
"reviewId"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "hupu/mentions",
|
||||
"file": "clis/hupu/mentions.js",
|
||||
"missing": [
|
||||
"has_next_page",
|
||||
"msg_type",
|
||||
"next_page_str",
|
||||
"topic_id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "indeed/job",
|
||||
"file": "clis/indeed/job.js",
|
||||
"missing": [
|
||||
"challenge",
|
||||
"jobType",
|
||||
"notFound",
|
||||
"ready"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "indeed/search",
|
||||
"file": "clis/indeed/search.js",
|
||||
"missing": [
|
||||
"jk"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "instagram/note",
|
||||
"file": "clis/instagram/note.js",
|
||||
"missing": [
|
||||
"stage",
|
||||
"text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "instagram/post",
|
||||
"file": "clis/instagram/post.js",
|
||||
"missing": [
|
||||
"failed",
|
||||
"settled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "instagram/post",
|
||||
"file": "clis/instagram/post.js",
|
||||
"missing": [
|
||||
"state"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "instagram/reel",
|
||||
"file": "clis/instagram/reel.js",
|
||||
"missing": [
|
||||
"failed",
|
||||
"settled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "instagram/reel",
|
||||
"file": "clis/instagram/reel.js",
|
||||
"missing": [
|
||||
"state"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "jd/item",
|
||||
"file": "clis/jd/item.js",
|
||||
"missing": [
|
||||
"hasProductMarker",
|
||||
"hasSecurityChallenge",
|
||||
"href",
|
||||
"isLoginPage",
|
||||
"isProductPage",
|
||||
"looksBlocked",
|
||||
"onExpectedItemUrl"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "jianyu/search",
|
||||
"file": "clis/jianyu/search.js",
|
||||
"missing": [
|
||||
"contextText",
|
||||
"date"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "jianyu/search",
|
||||
"file": "clis/jianyu/search.js",
|
||||
"missing": [
|
||||
"detail_reason"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "jianyu/search",
|
||||
"file": "clis/jianyu/search.js",
|
||||
"missing": [
|
||||
"detail_reason",
|
||||
"notice_id",
|
||||
"source_id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "jike/notifications",
|
||||
"file": "clis/jike/notifications.js",
|
||||
"missing": [
|
||||
"actionType",
|
||||
"fromUser"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "jike/topic",
|
||||
"file": "clis/jike/topic.js",
|
||||
"missing": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "ke/chengjiao",
|
||||
"file": "clis/ke/chengjiao.js",
|
||||
"missing": [
|
||||
"url"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "ke/xiaoqu",
|
||||
"file": "clis/ke/xiaoqu.js",
|
||||
"missing": [
|
||||
"url"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "linkedin/timeline",
|
||||
"file": "clis/linkedin/timeline.js",
|
||||
"missing": [
|
||||
"authorUrl",
|
||||
"postedAt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "linkedin/timeline",
|
||||
"file": "clis/linkedin/timeline.js",
|
||||
"missing": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "linkedin/timeline",
|
||||
"file": "clis/linkedin/timeline.js",
|
||||
"missing": [
|
||||
"postedAt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "linux-do/tags",
|
||||
"file": "clis/linux-do/tags.js",
|
||||
"missing": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "paperreview/review",
|
||||
"file": "clis/paperreview/review.js",
|
||||
"missing": [
|
||||
"message",
|
||||
"token"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "powerchina/search",
|
||||
"file": "clis/powerchina/search.js",
|
||||
"missing": [
|
||||
"contextText",
|
||||
"date"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "producthunt/hot",
|
||||
"file": "clis/producthunt/hot.js",
|
||||
"missing": [
|
||||
"voteCandidates"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "tieba/search",
|
||||
"file": "clis/tieba/search.js",
|
||||
"missing": [
|
||||
"snippet"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "twitter/accept",
|
||||
"file": "clis/twitter/accept.js",
|
||||
"missing": [
|
||||
"href",
|
||||
"idx",
|
||||
"text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "twitter/bookmarks",
|
||||
"file": "clis/twitter/bookmarks.js",
|
||||
"missing": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "twitter/list-remove",
|
||||
"file": "clis/twitter/list-remove.js",
|
||||
"missing": [
|
||||
"method",
|
||||
"ts",
|
||||
"url"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "twitter/list-tweets",
|
||||
"file": "clis/twitter/list-tweets.js",
|
||||
"missing": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "twitter/reply-dm",
|
||||
"file": "clis/twitter/reply-dm.js",
|
||||
"missing": [
|
||||
"convId",
|
||||
"href",
|
||||
"idx",
|
||||
"preview"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "twitter/thread",
|
||||
"file": "clis/twitter/thread.js",
|
||||
"missing": [
|
||||
"created_at",
|
||||
"in_reply_to"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "twitter/tweets",
|
||||
"file": "clis/twitter/tweets.js",
|
||||
"missing": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "uiverse/code",
|
||||
"file": "clis/uiverse/code.js",
|
||||
"missing": [
|
||||
"code",
|
||||
"isTailwind",
|
||||
"postId",
|
||||
"type",
|
||||
"url"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "uiverse/preview",
|
||||
"file": "clis/uiverse/preview.js",
|
||||
"missing": [
|
||||
"matchedClassName",
|
||||
"matchedTag",
|
||||
"postId",
|
||||
"selectorSource",
|
||||
"url",
|
||||
"x",
|
||||
"y"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "v2ex/daily",
|
||||
"file": "clis/v2ex/daily.js",
|
||||
"missing": [
|
||||
"claimed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "v2ex/daily",
|
||||
"file": "clis/v2ex/daily.js",
|
||||
"missing": [
|
||||
"claimed",
|
||||
"once"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "web/read",
|
||||
"file": "clis/web/read.js",
|
||||
"missing": [
|
||||
"accessible",
|
||||
"index",
|
||||
"sameOrigin",
|
||||
"src",
|
||||
"textLength"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "web/read",
|
||||
"file": "clis/web/read.js",
|
||||
"missing": [
|
||||
"bodyTruncated",
|
||||
"contentType",
|
||||
"method",
|
||||
"url"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "weibo/me",
|
||||
"file": "clis/weibo/me.js",
|
||||
"missing": [
|
||||
"avatar",
|
||||
"description",
|
||||
"profile_url"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "weibo/user",
|
||||
"file": "clis/weibo/user.js",
|
||||
"missing": [
|
||||
"avatar",
|
||||
"birthday",
|
||||
"created_at",
|
||||
"gender",
|
||||
"ip_location",
|
||||
"verified_reason"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "weread/book",
|
||||
"file": "clis/weread/book.js",
|
||||
"missing": [
|
||||
"metadataReady"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "weread/book",
|
||||
"file": "clis/weread/book.js",
|
||||
"missing": [
|
||||
"url"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "xianyu/item",
|
||||
"file": "clis/xianyu/item.js",
|
||||
"missing": [
|
||||
"browse_count",
|
||||
"category",
|
||||
"collect_count",
|
||||
"description",
|
||||
"image_count",
|
||||
"image_urls",
|
||||
"item_url",
|
||||
"original_price",
|
||||
"reply_interval",
|
||||
"reply_ratio_24h",
|
||||
"seller_id",
|
||||
"seller_score",
|
||||
"seller_url",
|
||||
"status"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "xianyu/search",
|
||||
"file": "clis/xianyu/search.js",
|
||||
"missing": [
|
||||
"extra",
|
||||
"original_price"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "xiaohongshu/creator-note-detail",
|
||||
"file": "clis/xiaohongshu/creator-note-detail.js",
|
||||
"missing": [
|
||||
"label"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "xiaohongshu/creator-notes-summary",
|
||||
"file": "clis/xiaohongshu/creator-notes-summary.js",
|
||||
"missing": [
|
||||
"published_at",
|
||||
"top_interest_pct",
|
||||
"top_source_pct"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "xiaohongshu/creator-notes",
|
||||
"file": "clis/xiaohongshu/creator-notes-summary.js",
|
||||
"missing": [
|
||||
"avg_view_time",
|
||||
"published_at",
|
||||
"rise_fans",
|
||||
"shares",
|
||||
"top_interest",
|
||||
"top_interest_pct",
|
||||
"top_source",
|
||||
"top_source_pct"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "xiaohongshu/download",
|
||||
"file": "clis/xiaohongshu/download.js",
|
||||
"missing": [
|
||||
"url"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "xiaohongshu/search",
|
||||
"file": "clis/xiaohongshu/search.js",
|
||||
"missing": [
|
||||
"author_url"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "xueqiu/comments",
|
||||
"file": "clis/xueqiu/comments.js",
|
||||
"missing": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "xueqiu/earnings-date",
|
||||
"file": "clis/xueqiu/earnings-date.js",
|
||||
"missing": [
|
||||
"_future",
|
||||
"_ts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "xueqiu/hot-stock",
|
||||
"file": "clis/xueqiu/hot-stock.js",
|
||||
"missing": [
|
||||
"url"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "xueqiu/kline",
|
||||
"file": "clis/xueqiu/kline.js",
|
||||
"missing": [
|
||||
"percent",
|
||||
"symbol"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "xueqiu/watchlist",
|
||||
"file": "clis/xueqiu/watchlist.js",
|
||||
"missing": [
|
||||
"change",
|
||||
"url",
|
||||
"volume"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "yahoo-finance/quote",
|
||||
"file": "clis/yahoo-finance/quote.js",
|
||||
"missing": [
|
||||
"currency",
|
||||
"exchange"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "yollomi/upload",
|
||||
"file": "clis/yollomi/upload.js",
|
||||
"missing": [
|
||||
"data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "youtube/playlist",
|
||||
"file": "clis/youtube/playlist.js",
|
||||
"missing": [
|
||||
"channelName",
|
||||
"stats",
|
||||
"videos"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "youtube/watch-later",
|
||||
"file": "clis/youtube/watch-later.js",
|
||||
"missing": [
|
||||
"stats",
|
||||
"videos"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "zhihu/hot",
|
||||
"file": "clis/zhihu/hot.js",
|
||||
"missing": [
|
||||
"answer_count",
|
||||
"follower_count",
|
||||
"url"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "zhihu/hot",
|
||||
"file": "clis/zhihu/hot.js",
|
||||
"missing": [
|
||||
"url"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "zsxq/groups",
|
||||
"file": "clis/zsxq/groups.js",
|
||||
"missing": [
|
||||
"valid_until"
|
||||
]
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user