@onebullex-group/cli 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/README.md +243 -0
- package/dist/chunk-AKGCHJPQ.js +88 -0
- package/dist/chunk-CVOY2SOS.js +139 -0
- package/dist/chunk-FTINNKNR.js +492 -0
- package/dist/chunk-J3TAAD3Q.js +58 -0
- package/dist/chunk-JRUFIHU7.js +45 -0
- package/dist/chunk-LVNSFQXS.js +31 -0
- package/dist/chunk-OOLZIYXS.js +41 -0
- package/dist/chunk-ZYO5P4DH.js +19 -0
- package/dist/core/config.d.ts +44 -0
- package/dist/core/config.js +19 -0
- package/dist/core/errors.d.ts +23 -0
- package/dist/core/errors.js +13 -0
- package/dist/core/http-client.d.ts +29 -0
- package/dist/core/http-client.js +13 -0
- package/dist/core/redact.d.ts +15 -0
- package/dist/core/redact.js +10 -0
- package/dist/core/signature.d.ts +39 -0
- package/dist/core/signature.js +11 -0
- package/dist/core/types.d.ts +46 -0
- package/dist/core/types.js +7 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +1627 -0
- package/dist/lint-api.d.ts +38 -0
- package/dist/lint-api.js +257 -0
- package/dist/output-UIZGG7EU.js +13 -0
- package/dist/registry.d.ts +5 -0
- package/dist/registry.js +7 -0
- package/package.json +54 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { CommandRegEntry } from './core/types.js';
|
|
2
|
+
|
|
3
|
+
interface Endpoint {
|
|
4
|
+
method: 'GET' | 'POST';
|
|
5
|
+
path: string;
|
|
6
|
+
}
|
|
7
|
+
interface DocParam {
|
|
8
|
+
name: string;
|
|
9
|
+
required: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* In Phase 2 every documented endpoint has a CLI command. The list is
|
|
13
|
+
* intentionally empty; the constant is kept as a hook for staging Phase 3
|
|
14
|
+
* additions if needed.
|
|
15
|
+
*/
|
|
16
|
+
declare const PHASE1_DEFERRED: readonly Endpoint[];
|
|
17
|
+
declare function parseDocEndpoints(md: string): Endpoint[];
|
|
18
|
+
declare function diffEndpoints(doc: readonly Endpoint[], cli: readonly Endpoint[]): {
|
|
19
|
+
missingInCli: Endpoint[];
|
|
20
|
+
extraInCli: Endpoint[];
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Extract `(name, required)` pairs from the request param table that follows the
|
|
24
|
+
* `#### METHOD <path>` heading. Supports both shapes:
|
|
25
|
+
* - GET / query table: 5 columns `参数 | 位置 | 类型 | 必填 | 说明`
|
|
26
|
+
* - POST body table: 4 columns `字段 | 类型 | 必填 | 说明`
|
|
27
|
+
*/
|
|
28
|
+
declare function parseDocParams(md: string, path: string): DocParam[];
|
|
29
|
+
/**
|
|
30
|
+
* Parses a TS source string and returns commander positional/option flags as
|
|
31
|
+
* `(name, required)` pairs. Positional `<x>` → required, `[x]` → optional;
|
|
32
|
+
* `.requiredOption('--foo …')` → required, `.option('--foo …')` → optional.
|
|
33
|
+
*/
|
|
34
|
+
declare function extractCliParamsFromSource(source: string): DocParam[];
|
|
35
|
+
declare function diffParams(endpoint: Endpoint, docParams: readonly DocParam[], cliParams: readonly DocParam[]): string[];
|
|
36
|
+
declare function checkWriteSafety(reg?: readonly CommandRegEntry[]): string[];
|
|
37
|
+
|
|
38
|
+
export { type DocParam, type Endpoint, PHASE1_DEFERRED, checkWriteSafety, diffEndpoints, diffParams, extractCliParamsFromSource, parseDocEndpoints, parseDocParams };
|
package/dist/lint-api.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
REGISTRY
|
|
4
|
+
} from "./chunk-FTINNKNR.js";
|
|
5
|
+
|
|
6
|
+
// scripts/check-cli-coverage.ts
|
|
7
|
+
import { readFileSync } from "fs";
|
|
8
|
+
import { resolve } from "path";
|
|
9
|
+
import { Project, SyntaxKind } from "ts-morph";
|
|
10
|
+
var PHASE1_DEFERRED = [];
|
|
11
|
+
var ENDPOINT_RE = /^####\s+(GET|POST)\s+(\/v2\/\S+)/gm;
|
|
12
|
+
function parseDocEndpoints(md) {
|
|
13
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14
|
+
const out = [];
|
|
15
|
+
ENDPOINT_RE.lastIndex = 0;
|
|
16
|
+
let m;
|
|
17
|
+
while ((m = ENDPOINT_RE.exec(md)) !== null) {
|
|
18
|
+
const method = m[1];
|
|
19
|
+
const path = m[2].replace(/[—,;:]+$/, "");
|
|
20
|
+
const key = `${method} ${path}`;
|
|
21
|
+
if (!seen.has(key)) {
|
|
22
|
+
seen.add(key);
|
|
23
|
+
out.push({ method, path });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
function diffEndpoints(doc, cli) {
|
|
29
|
+
const k = (e) => `${e.method} ${e.path}`;
|
|
30
|
+
const docSet = new Set(doc.map(k));
|
|
31
|
+
const cliSet = new Set(cli.map(k));
|
|
32
|
+
return {
|
|
33
|
+
missingInCli: doc.filter((e) => !cliSet.has(k(e))),
|
|
34
|
+
extraInCli: cli.filter((e) => !docSet.has(k(e)))
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function escapeRegex(s) {
|
|
38
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
39
|
+
}
|
|
40
|
+
function parseDocParams(md, path) {
|
|
41
|
+
const headingRe = new RegExp(
|
|
42
|
+
`####\\s+(?:GET|POST)\\s+${escapeRegex(path)}\\b`,
|
|
43
|
+
"m"
|
|
44
|
+
);
|
|
45
|
+
const m = headingRe.exec(md);
|
|
46
|
+
if (!m) return [];
|
|
47
|
+
const after = md.slice(m.index + m[0].length);
|
|
48
|
+
const nextSection = /^####\s+/m.exec(after);
|
|
49
|
+
const section = nextSection ? after.slice(0, nextSection.index) : after;
|
|
50
|
+
const tableRe = /\|\s*(?:参数|字段)\s*\|[^\n]*\|\s*必填\s*\|[^\n]*\n\|[-:\s|]+\|\n((?:\|[^\n]*\n)+)/;
|
|
51
|
+
const t = tableRe.exec(section);
|
|
52
|
+
if (!t) return [];
|
|
53
|
+
const headerEnd = t[0].indexOf("\n");
|
|
54
|
+
const headerLine = t[0].slice(0, headerEnd);
|
|
55
|
+
const headerCells = headerLine.split("|").map((c) => c.trim()).slice(1, -1);
|
|
56
|
+
const requiredIdx = headerCells.indexOf("\u5FC5\u586B");
|
|
57
|
+
if (requiredIdx < 0) return [];
|
|
58
|
+
const rows = (t[1] ?? "").split("\n").filter((l) => l.trim().startsWith("|"));
|
|
59
|
+
const out = [];
|
|
60
|
+
for (const row of rows) {
|
|
61
|
+
const cells = row.split("|").map((c) => c.trim()).slice(1, -1);
|
|
62
|
+
if (cells.length < headerCells.length) continue;
|
|
63
|
+
const nameCell = cells[0] ?? "";
|
|
64
|
+
const reqCell = cells[requiredIdx] ?? "";
|
|
65
|
+
const nameMatch = /^`([^`]+)`$/.exec(nameCell);
|
|
66
|
+
if (!nameMatch) continue;
|
|
67
|
+
out.push({ name: nameMatch[1] ?? "", required: reqCell === "\u662F" });
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
function unquote(s) {
|
|
72
|
+
if (s.startsWith("'") && s.endsWith("'") || s.startsWith('"') && s.endsWith('"') || s.startsWith("`") && s.endsWith("`")) {
|
|
73
|
+
return s.slice(1, -1);
|
|
74
|
+
}
|
|
75
|
+
return s;
|
|
76
|
+
}
|
|
77
|
+
function extractCliParamsFromSource(source) {
|
|
78
|
+
const project = new Project({ useInMemoryFileSystem: true });
|
|
79
|
+
const f = project.createSourceFile("temp.ts", source);
|
|
80
|
+
const out = [];
|
|
81
|
+
for (const call of f.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
82
|
+
const expr = call.getExpression();
|
|
83
|
+
const text = expr.getText();
|
|
84
|
+
if (text.endsWith(".command")) {
|
|
85
|
+
const arg = call.getArguments()[0];
|
|
86
|
+
if (arg) {
|
|
87
|
+
const sigText = unquote(arg.getText());
|
|
88
|
+
const tokens = sigText.split(/\s+/).slice(1);
|
|
89
|
+
for (const tok of tokens) {
|
|
90
|
+
if (tok.startsWith("<") && tok.endsWith(">")) {
|
|
91
|
+
out.push({ name: tok.slice(1, -1), required: true });
|
|
92
|
+
} else if (tok.startsWith("[") && tok.endsWith("]")) {
|
|
93
|
+
out.push({ name: tok.slice(1, -1), required: false });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (text.endsWith(".requiredOption") || text.endsWith(".option")) {
|
|
100
|
+
const arg = call.getArguments()[0];
|
|
101
|
+
if (arg) {
|
|
102
|
+
const flag = unquote(arg.getText());
|
|
103
|
+
const m = /--([a-z][a-z0-9-]*)/i.exec(flag);
|
|
104
|
+
if (m && m[1]) {
|
|
105
|
+
out.push({ name: m[1], required: text.endsWith(".requiredOption") });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
var GLOBAL_FLAGS = /* @__PURE__ */ new Set([
|
|
113
|
+
"profile",
|
|
114
|
+
"api-key",
|
|
115
|
+
"secret",
|
|
116
|
+
"base-url",
|
|
117
|
+
"output",
|
|
118
|
+
"debug",
|
|
119
|
+
"timeout",
|
|
120
|
+
"confirm",
|
|
121
|
+
"reveal",
|
|
122
|
+
"stdin",
|
|
123
|
+
"value",
|
|
124
|
+
"name",
|
|
125
|
+
"env",
|
|
126
|
+
"identify",
|
|
127
|
+
"set-default",
|
|
128
|
+
"yes"
|
|
129
|
+
]);
|
|
130
|
+
function normalize(name) {
|
|
131
|
+
return name.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/_/g, "-").toLowerCase();
|
|
132
|
+
}
|
|
133
|
+
function diffParams(endpoint, docParams, cliParams) {
|
|
134
|
+
const errors = [];
|
|
135
|
+
const docMap = new Map(
|
|
136
|
+
docParams.map((p) => [normalize(p.name), p])
|
|
137
|
+
);
|
|
138
|
+
const cliMap = new Map(
|
|
139
|
+
cliParams.filter((p) => !GLOBAL_FLAGS.has(p.name)).map((p) => [normalize(p.name), p])
|
|
140
|
+
);
|
|
141
|
+
for (const [k, dp] of docMap.entries()) {
|
|
142
|
+
if (dp.required) {
|
|
143
|
+
const cp = cliMap.get(k);
|
|
144
|
+
if (!cp) {
|
|
145
|
+
errors.push(`${endpoint.method} ${endpoint.path}: missing required CLI option --${k}`);
|
|
146
|
+
} else if (!cp.required) {
|
|
147
|
+
errors.push(
|
|
148
|
+
`${endpoint.method} ${endpoint.path}: --${k} should be requiredOption (doc says \u5FC5\u586B=\u662F)`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const k of cliMap.keys()) {
|
|
154
|
+
if (!docMap.has(k)) {
|
|
155
|
+
errors.push(
|
|
156
|
+
`${endpoint.method} ${endpoint.path}: CLI defines --${k} not present in doc params`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return errors;
|
|
161
|
+
}
|
|
162
|
+
var READ_ONLY_POST_ALLOWLIST = /* @__PURE__ */ new Set(["/v2/order/list-by-ids"]);
|
|
163
|
+
function checkWriteSafety(reg = REGISTRY) {
|
|
164
|
+
const errors = [];
|
|
165
|
+
for (const e of reg) {
|
|
166
|
+
if (e.isWrite && e.method === "GET") {
|
|
167
|
+
errors.push(`${e.command}: isWrite=true but method=GET (suspicious)`);
|
|
168
|
+
}
|
|
169
|
+
if (!e.isWrite && e.method === "POST" && !READ_ONLY_POST_ALLOWLIST.has(e.path)) {
|
|
170
|
+
errors.push(
|
|
171
|
+
`${e.command}: method=POST but isWrite=false (likely missing isWrite flag for ${e.path})`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return errors;
|
|
176
|
+
}
|
|
177
|
+
async function main(opts = {}) {
|
|
178
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
179
|
+
const log = opts.log ?? ((s) => process.stdout.write(`${s}
|
|
180
|
+
`));
|
|
181
|
+
const errLog = opts.errLog ?? ((s) => process.stderr.write(`${s}
|
|
182
|
+
`));
|
|
183
|
+
const mdPath = resolve(cwd, "..", "docs", "onebullex-openapi-cn-v2.md");
|
|
184
|
+
const md = readFileSync(mdPath, "utf8");
|
|
185
|
+
const allDoc = parseDocEndpoints(md);
|
|
186
|
+
const deferredKey = (e) => `${e.method} ${e.path}`;
|
|
187
|
+
const deferredSet = new Set(PHASE1_DEFERRED.map(deferredKey));
|
|
188
|
+
const docEndpoints = allDoc.filter((e) => !deferredSet.has(deferredKey(e)));
|
|
189
|
+
const cliEndpoints = REGISTRY.map((e) => ({ method: e.method, path: e.path }));
|
|
190
|
+
const { missingInCli, extraInCli } = diffEndpoints(docEndpoints, cliEndpoints);
|
|
191
|
+
let ok = true;
|
|
192
|
+
if (missingInCli.length > 0) {
|
|
193
|
+
ok = false;
|
|
194
|
+
errLog(`
|
|
195
|
+
[Endpoint coverage] Missing in CLI (${missingInCli.length}):`);
|
|
196
|
+
for (const e of missingInCli) errLog(` ${e.method.padEnd(4)} ${e.path}`);
|
|
197
|
+
}
|
|
198
|
+
if (extraInCli.length > 0) {
|
|
199
|
+
ok = false;
|
|
200
|
+
errLog(`
|
|
201
|
+
[Endpoint coverage] Extra in CLI (${extraInCli.length}):`);
|
|
202
|
+
for (const e of extraInCli) errLog(` ${e.method.padEnd(4)} ${e.path}`);
|
|
203
|
+
}
|
|
204
|
+
const paramErrors = [];
|
|
205
|
+
for (const entry of REGISTRY) {
|
|
206
|
+
const docParams = parseDocParams(md, entry.path);
|
|
207
|
+
if (docParams.length === 0) continue;
|
|
208
|
+
const implPath = resolve(cwd, entry.implFile);
|
|
209
|
+
let src;
|
|
210
|
+
try {
|
|
211
|
+
src = readFileSync(implPath, "utf8");
|
|
212
|
+
} catch {
|
|
213
|
+
paramErrors.push(`${entry.command}: cannot read ${entry.implFile}`);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const cliParams = extractCliParamsFromSource(src);
|
|
217
|
+
paramErrors.push(...diffParams({ method: entry.method, path: entry.path }, docParams, cliParams));
|
|
218
|
+
}
|
|
219
|
+
if (paramErrors.length > 0) {
|
|
220
|
+
ok = false;
|
|
221
|
+
errLog(`
|
|
222
|
+
[Parameter coverage] ${paramErrors.length} mismatch(es):`);
|
|
223
|
+
for (const e of paramErrors) errLog(` ${e}`);
|
|
224
|
+
}
|
|
225
|
+
const writeErrors = checkWriteSafety(REGISTRY);
|
|
226
|
+
if (writeErrors.length > 0) {
|
|
227
|
+
ok = false;
|
|
228
|
+
errLog(`
|
|
229
|
+
[Write-op safety] ${writeErrors.length} issue(s):`);
|
|
230
|
+
for (const e of writeErrors) errLog(` ${e}`);
|
|
231
|
+
}
|
|
232
|
+
if (!ok) {
|
|
233
|
+
errLog("\nEdit cli/src/registry.ts and add/remove command files to match the docs.");
|
|
234
|
+
return 1;
|
|
235
|
+
}
|
|
236
|
+
log(
|
|
237
|
+
`OK V2 endpoint coverage: ${cliEndpoints.length} commands cover ${docEndpoints.length} doc endpoints. Parameter and write-safety lints passed.`
|
|
238
|
+
);
|
|
239
|
+
return 0;
|
|
240
|
+
}
|
|
241
|
+
var isMainModule = () => {
|
|
242
|
+
const argv1 = process.argv[1];
|
|
243
|
+
if (!argv1) return false;
|
|
244
|
+
return argv1.endsWith("check-cli-coverage.ts") || argv1.endsWith("check-cli-coverage.js");
|
|
245
|
+
};
|
|
246
|
+
if (isMainModule()) {
|
|
247
|
+
void main().then((code) => process.exit(code));
|
|
248
|
+
}
|
|
249
|
+
export {
|
|
250
|
+
PHASE1_DEFERRED,
|
|
251
|
+
checkWriteSafety,
|
|
252
|
+
diffEndpoints,
|
|
253
|
+
diffParams,
|
|
254
|
+
extractCliParamsFromSource,
|
|
255
|
+
parseDocEndpoints,
|
|
256
|
+
parseDocParams
|
|
257
|
+
};
|
package/dist/registry.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@onebullex-group/cli",
|
|
3
|
+
"version": "1.1.2",
|
|
4
|
+
"description": "OneBullEx Open API V2 command-line interface",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"onebullex": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./dist/index.js",
|
|
12
|
+
"./registry": "./dist/registry.js",
|
|
13
|
+
"./lint": "./dist/lint-api.js",
|
|
14
|
+
"./core/http-client": "./dist/core/http-client.js",
|
|
15
|
+
"./core/config": "./dist/core/config.js",
|
|
16
|
+
"./core/errors": "./dist/core/errors.js",
|
|
17
|
+
"./core/signature": "./dist/core/signature.js",
|
|
18
|
+
"./core/types": "./dist/core/types.js",
|
|
19
|
+
"./core/redact": "./dist/core/redact.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md",
|
|
24
|
+
"CHANGELOG.md"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"cli-table3": "^0.6.5",
|
|
31
|
+
"commander": "^12.1.0",
|
|
32
|
+
"ts-morph": "^23.0.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@biomejs/biome": "^1.9.4",
|
|
36
|
+
"@types/node": "^20.14.0",
|
|
37
|
+
"nock": "^13.5.5",
|
|
38
|
+
"tsup": "^8.3.0",
|
|
39
|
+
"tsx": "^4.19.0",
|
|
40
|
+
"typescript": "^5.5.0",
|
|
41
|
+
"undici": "^8.3.0",
|
|
42
|
+
"vitest": "^2.1.0"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsup",
|
|
46
|
+
"dev": "tsx src/index.ts",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"test:e2e": "vitest run tests/e2e",
|
|
49
|
+
"lint": "biome lint src tests",
|
|
50
|
+
"format": "biome format --write src tests",
|
|
51
|
+
"check-coverage": "tsx scripts/check-cli-coverage.ts",
|
|
52
|
+
"typecheck": "tsc --noEmit"
|
|
53
|
+
}
|
|
54
|
+
}
|