@openpkg-ts/cli 0.11.3 → 0.12.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.
- package/README.md +26 -5
- package/dist/index.js +137 -27
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -5,9 +5,15 @@ Extract [OpenPkg](https://openpkg.dev) documents and generate docs from the comm
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
# Extract an
|
|
8
|
+
# Extract from an entry file
|
|
9
9
|
bunx @openpkg-ts/cli spec src/index.ts -o openpkg.json
|
|
10
10
|
|
|
11
|
+
# Or resolve the package/entry from a dir, cwd, intent, or git URL
|
|
12
|
+
bunx @openpkg-ts/cli spec
|
|
13
|
+
bunx @openpkg-ts/cli spec .
|
|
14
|
+
bunx @openpkg-ts/cli spec . sdk
|
|
15
|
+
bunx @openpkg-ts/cli spec https://github.com/org/repo
|
|
16
|
+
|
|
11
17
|
# Generate markdown docs (from source or an existing spec)
|
|
12
18
|
bunx @openpkg-ts/cli docs src/index.ts -o docs/api.md
|
|
13
19
|
bunx @openpkg-ts/cli docs openpkg.json -f html -o docs/api.html
|
|
@@ -19,16 +25,31 @@ bunx @openpkg-ts/cli list src/index.ts
|
|
|
19
25
|
bunx @openpkg-ts/cli diff old.json new.json
|
|
20
26
|
```
|
|
21
27
|
|
|
28
|
+
Prefers TypeScript source (`src/index.ts`) over `dist/*.d.ts`. Several packages and no intent → prompt (or a list if not a TTY).
|
|
29
|
+
|
|
30
|
+
Opt-in Jev routing (needs `AI_GATEWAY_API_KEY` and the `ai` package). Sends package.json + file heads to Vercel AI Gateway with zero data retention:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bunx @openpkg-ts/cli spec . --jev
|
|
34
|
+
bunx @openpkg-ts/cli spec . --jev --follow-external auto
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`followExternal: "auto"` (config or flag) requires `--jev`. Config: `openpkg.config.json` or `package.json#openpkg`.
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{ "followExternal": "auto", "decisions": "jev" }
|
|
41
|
+
```
|
|
42
|
+
|
|
22
43
|
## Commands
|
|
23
44
|
|
|
24
45
|
| Command | Description |
|
|
25
46
|
|---------|-------------|
|
|
26
|
-
| `spec
|
|
27
|
-
| `docs
|
|
28
|
-
| `list
|
|
47
|
+
| `spec [path \| entry.ts] [intent...]` | Extract a spec from a file, package dir, cwd, or git URL |
|
|
48
|
+
| `docs [path \| entry.ts \| spec.json] [intent...]` | Generate docs (`-f md\|html\|json`) |
|
|
49
|
+
| `list [path \| entry.ts] [intent...]` | List exports with kind and location (`--json`) |
|
|
29
50
|
| `diff <old.json> <new.json>` | Compare specs; exits 2 if breaking changes |
|
|
30
51
|
|
|
31
|
-
`-o, --output` writes to a file instead of stdout.
|
|
52
|
+
`-o, --output` writes to a file instead of stdout. `--jev` routes package/entry with Jev. `--follow-external auto` expands load-bearing externals (requires `--jev`).
|
|
32
53
|
|
|
33
54
|
For programmatic use, richer options, and framework integrations (search indexes, nav trees), use `@openpkg-ts/sdk` directly.
|
|
34
55
|
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import readline from "node:readline/promises";
|
|
6
7
|
import { parseArgs } from "node:util";
|
|
7
8
|
import {
|
|
8
9
|
calculateNextVersion,
|
|
@@ -12,23 +13,26 @@ import {
|
|
|
12
13
|
extractSpec,
|
|
13
14
|
getAvailableVersions,
|
|
14
15
|
getValidationErrors,
|
|
16
|
+
isRemoteInput,
|
|
15
17
|
listExports,
|
|
16
18
|
loadConfig,
|
|
17
19
|
mergeConfig,
|
|
18
|
-
|
|
20
|
+
pickEntry,
|
|
21
|
+
recommendSemverBump,
|
|
22
|
+
resolveTarget
|
|
19
23
|
} from "@openpkg-ts/sdk";
|
|
20
24
|
var HELP = `openpkg - extract TypeScript API specs and generate docs
|
|
21
25
|
|
|
22
26
|
Usage:
|
|
23
|
-
openpkg spec
|
|
24
|
-
openpkg docs
|
|
25
|
-
openpkg list
|
|
27
|
+
openpkg spec [path | entry.ts] [intent...] [-o spec.json] [--follow-external <pkg,...>]
|
|
28
|
+
openpkg docs [path | entry.ts | spec.json] [intent...] [-f md|html|json] [-o out]
|
|
29
|
+
openpkg list [path | entry.ts] [intent...] [--json]
|
|
26
30
|
openpkg validate <spec.json>
|
|
27
31
|
openpkg diff <old.json> <new.json> [--json]
|
|
28
32
|
|
|
29
33
|
Commands:
|
|
30
|
-
spec Extract an OpenPkg spec
|
|
31
|
-
docs Generate docs from
|
|
34
|
+
spec Extract an OpenPkg spec (dir, cwd, or entry file)
|
|
35
|
+
docs Generate docs from a package, entry point, or spec file
|
|
32
36
|
list List exports (name, kind, location)
|
|
33
37
|
validate Validate a spec file against the OpenPkg meta-schema
|
|
34
38
|
diff Compare two spec files and recommend a semver bump
|
|
@@ -38,16 +42,19 @@ Options:
|
|
|
38
42
|
-f, --format docs output format: md (default), html, json
|
|
39
43
|
--json list/diff output as JSON
|
|
40
44
|
--follow-external Expand types from these packages (comma-separated,
|
|
41
|
-
globs ok: "@ai-sdk/*"
|
|
45
|
+
globs ok: "@ai-sdk/*", or "auto" with --jev).
|
|
46
|
+
Default: stub externals.
|
|
42
47
|
--follow-external-all Expand every external package (use with care)
|
|
43
48
|
--only Only extract these exports (comma-separated, * ok)
|
|
44
49
|
--ignore Ignore these exports (comma-separated, * ok)
|
|
50
|
+
--jev Route package/entry with Jev (needs AI_GATEWAY_API_KEY)
|
|
45
51
|
-h, --help Show this help
|
|
46
52
|
-v, --version Show version
|
|
47
53
|
|
|
48
54
|
Config: reads openpkg.config.json (or package.json "openpkg" field) from the
|
|
49
|
-
cwd. Flags override the file. Example
|
|
50
|
-
{ "followExternal": ["@
|
|
55
|
+
cwd. Flags override the file. Example:
|
|
56
|
+
{ "followExternal": ["@ai-sdk/*"] }
|
|
57
|
+
{ "followExternal": "auto", "decisions": "jev" }
|
|
51
58
|
`;
|
|
52
59
|
function fail(message) {
|
|
53
60
|
console.error(`error: ${message}`);
|
|
@@ -76,12 +83,111 @@ function reportDiagnostics(diagnostics) {
|
|
|
76
83
|
process.exit(1);
|
|
77
84
|
}
|
|
78
85
|
}
|
|
86
|
+
function parseTargetArgs(positionals, cwd) {
|
|
87
|
+
if (!positionals.length)
|
|
88
|
+
return { input: cwd };
|
|
89
|
+
const first = positionals[0];
|
|
90
|
+
const rest = positionals.slice(1).join(" ").trim();
|
|
91
|
+
const abs = path.resolve(cwd, first);
|
|
92
|
+
if (isRemoteInput(first)) {
|
|
93
|
+
return { input: first, ...rest ? { intent: rest } : {} };
|
|
94
|
+
}
|
|
95
|
+
if (fs.existsSync(abs)) {
|
|
96
|
+
return { input: abs, ...rest ? { intent: rest } : {} };
|
|
97
|
+
}
|
|
98
|
+
return { input: cwd, intent: positionals.join(" ") };
|
|
99
|
+
}
|
|
100
|
+
function formatPackages(candidates, cwd) {
|
|
101
|
+
return candidates.map((c, i) => {
|
|
102
|
+
const rel = path.relative(cwd, c.dir) || ".";
|
|
103
|
+
return ` ${i + 1}. ${c.name} ${rel}`;
|
|
104
|
+
}).join(`
|
|
105
|
+
`);
|
|
106
|
+
}
|
|
107
|
+
async function choosePackage(candidates, cwd) {
|
|
108
|
+
const body = `multiple packages — pick one:
|
|
109
|
+
${formatPackages(candidates, cwd)}`;
|
|
110
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
111
|
+
const hint = candidates[0]?.name.split("/").pop() ?? "sdk";
|
|
112
|
+
fail(`${body}
|
|
113
|
+
re-run with a path or intent, e.g. openpkg spec . ${hint}`);
|
|
114
|
+
}
|
|
115
|
+
console.error(body);
|
|
116
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
117
|
+
try {
|
|
118
|
+
const answer = await rl.question(`Package [1-${candidates.length}]: `);
|
|
119
|
+
const i = Number(answer.trim());
|
|
120
|
+
if (!Number.isInteger(i) || i < 1 || i > candidates.length)
|
|
121
|
+
fail("invalid selection");
|
|
122
|
+
return candidates[i - 1];
|
|
123
|
+
} finally {
|
|
124
|
+
rl.close();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function loadCwdEnv() {
|
|
128
|
+
for (const name of [".env.local", ".env"]) {
|
|
129
|
+
const file = path.join(process.cwd(), name);
|
|
130
|
+
if (!fs.existsSync(file))
|
|
131
|
+
continue;
|
|
132
|
+
for (const line of fs.readFileSync(file, "utf8").split(`
|
|
133
|
+
`)) {
|
|
134
|
+
const t = line.trim();
|
|
135
|
+
if (!t || t.startsWith("#"))
|
|
136
|
+
continue;
|
|
137
|
+
const i = t.indexOf("=");
|
|
138
|
+
if (i === -1)
|
|
139
|
+
continue;
|
|
140
|
+
const k = t.slice(0, i).trim();
|
|
141
|
+
const v = t.slice(i + 1).trim();
|
|
142
|
+
if (k && process.env[k] === undefined)
|
|
143
|
+
process.env[k] = v;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function resolveCliTarget(positionals, decisions) {
|
|
148
|
+
loadCwdEnv();
|
|
149
|
+
const cwd = process.cwd();
|
|
150
|
+
const { input, intent } = parseTargetArgs(positionals, cwd);
|
|
151
|
+
const resolved = await resolveTarget({ input, intent, cwd, decisions });
|
|
152
|
+
if (resolved.kind === "unavailable")
|
|
153
|
+
fail(resolved.reason);
|
|
154
|
+
if (resolved.kind === "remote")
|
|
155
|
+
fail("failed to clone remote repo");
|
|
156
|
+
if (resolved.kind === "empty")
|
|
157
|
+
fail(resolved.reason);
|
|
158
|
+
if (resolved.kind === "needs-build") {
|
|
159
|
+
console.error(`error: ${resolved.reason}`);
|
|
160
|
+
if (resolved.command)
|
|
161
|
+
console.error(` → ${resolved.command}`);
|
|
162
|
+
process.exit(2);
|
|
163
|
+
}
|
|
164
|
+
if (resolved.kind === "explicit") {
|
|
165
|
+
return { entryFile: resolved.entryFile, entryPointSource: resolved.entryPointSource };
|
|
166
|
+
}
|
|
167
|
+
if (resolved.kind === "ok") {
|
|
168
|
+
return { entryFile: resolved.entryFile, entryPointSource: resolved.entryPointSource };
|
|
169
|
+
}
|
|
170
|
+
const chosen = await choosePackage(resolved.candidates, cwd);
|
|
171
|
+
const picked = pickEntry(chosen.dir);
|
|
172
|
+
if (!picked)
|
|
173
|
+
fail(`no TypeScript entry found in ${chosen.name}`);
|
|
174
|
+
return picked;
|
|
175
|
+
}
|
|
79
176
|
function toList(value) {
|
|
80
177
|
if (!value)
|
|
81
178
|
return;
|
|
82
179
|
const items = value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
83
180
|
return items.length > 0 ? items : undefined;
|
|
84
181
|
}
|
|
182
|
+
function parseFollowExternal(value, all) {
|
|
183
|
+
if (all)
|
|
184
|
+
return true;
|
|
185
|
+
if (!value)
|
|
186
|
+
return;
|
|
187
|
+
if (value.trim() === "auto")
|
|
188
|
+
return "auto";
|
|
189
|
+
return toList(value);
|
|
190
|
+
}
|
|
85
191
|
function reportStubbedExternals(spec) {
|
|
86
192
|
const counts = new Map;
|
|
87
193
|
for (const t of spec.types ?? []) {
|
|
@@ -105,26 +211,31 @@ async function specCommand(args) {
|
|
|
105
211
|
"follow-external": { type: "string" },
|
|
106
212
|
"follow-external-all": { type: "boolean" },
|
|
107
213
|
only: { type: "string" },
|
|
108
|
-
ignore: { type: "string" }
|
|
214
|
+
ignore: { type: "string" },
|
|
215
|
+
jev: { type: "boolean" }
|
|
109
216
|
},
|
|
110
217
|
allowPositionals: true
|
|
111
218
|
});
|
|
112
|
-
const entryFile = positionals[0];
|
|
113
|
-
if (!entryFile)
|
|
114
|
-
fail("spec requires an entry file (openpkg spec src/index.ts)");
|
|
115
219
|
const fileConfig = loadConfig(process.cwd());
|
|
116
220
|
const cliConfig = {
|
|
117
|
-
followExternal: values["follow-external
|
|
221
|
+
followExternal: parseFollowExternal(values["follow-external"], values["follow-external-all"]),
|
|
118
222
|
only: toList(values.only),
|
|
119
|
-
ignore: toList(values.ignore)
|
|
223
|
+
ignore: toList(values.ignore),
|
|
224
|
+
...values.jev ? { decisions: "jev" } : {}
|
|
120
225
|
};
|
|
226
|
+
const { entryFile, entryPointSource } = await resolveCliTarget(positionals, cliConfig.decisions ?? fileConfig?.decisions);
|
|
121
227
|
const config = mergeConfig(fileConfig, cliConfig);
|
|
228
|
+
if (config.followExternal === "auto" && config.decisions !== "jev") {
|
|
229
|
+
fail("followExternal auto requires --jev");
|
|
230
|
+
}
|
|
122
231
|
const { spec, diagnostics } = await extractSpec({
|
|
123
232
|
entryFile,
|
|
233
|
+
entryPointSource,
|
|
124
234
|
followExternal: config.followExternal,
|
|
125
235
|
only: config.only,
|
|
126
236
|
ignore: config.ignore,
|
|
127
|
-
externals: config.externals
|
|
237
|
+
externals: config.externals,
|
|
238
|
+
decisions: config.decisions
|
|
128
239
|
});
|
|
129
240
|
reportDiagnostics(diagnostics);
|
|
130
241
|
if (!config.followExternal)
|
|
@@ -136,21 +247,21 @@ async function docsCommand(args) {
|
|
|
136
247
|
args,
|
|
137
248
|
options: {
|
|
138
249
|
output: { type: "string", short: "o" },
|
|
139
|
-
format: { type: "string", short: "f" }
|
|
250
|
+
format: { type: "string", short: "f" },
|
|
251
|
+
jev: { type: "boolean" }
|
|
140
252
|
},
|
|
141
253
|
allowPositionals: true
|
|
142
254
|
});
|
|
143
|
-
const input = positionals[0];
|
|
144
|
-
if (!input)
|
|
145
|
-
fail("docs requires an entry file or spec file (openpkg docs src/index.ts)");
|
|
146
255
|
const format = values.format ?? "md";
|
|
147
256
|
if (!["md", "html", "json"].includes(format))
|
|
148
257
|
fail(`unknown format "${format}" (md|html|json)`);
|
|
149
258
|
let docs;
|
|
150
|
-
if (
|
|
151
|
-
docs = createDocs(
|
|
259
|
+
if (positionals[0]?.endsWith(".json")) {
|
|
260
|
+
docs = createDocs(positionals[0]);
|
|
152
261
|
} else {
|
|
153
|
-
const
|
|
262
|
+
const decisions = values.jev ? "jev" : loadConfig(process.cwd())?.decisions;
|
|
263
|
+
const { entryFile, entryPointSource } = await resolveCliTarget(positionals, decisions);
|
|
264
|
+
const { spec, diagnostics } = await extractSpec({ entryFile, entryPointSource });
|
|
154
265
|
reportDiagnostics(diagnostics);
|
|
155
266
|
docs = createDocs(spec);
|
|
156
267
|
}
|
|
@@ -160,12 +271,11 @@ async function docsCommand(args) {
|
|
|
160
271
|
async function listCommand(args) {
|
|
161
272
|
const { values, positionals } = parseArgs({
|
|
162
273
|
args,
|
|
163
|
-
options: { json: { type: "boolean" } },
|
|
274
|
+
options: { json: { type: "boolean" }, jev: { type: "boolean" } },
|
|
164
275
|
allowPositionals: true
|
|
165
276
|
});
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
fail("list requires an entry file (openpkg list src/index.ts)");
|
|
277
|
+
const decisions = values.jev ? "jev" : loadConfig(process.cwd())?.decisions;
|
|
278
|
+
const { entryFile } = await resolveCliTarget(positionals, decisions);
|
|
169
279
|
const { exports, errors } = await listExports({ entryFile });
|
|
170
280
|
for (const err of errors) {
|
|
171
281
|
console.error(`error: ${err}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpkg-ts/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "CLI for OpenPkg - extract TypeScript API specs and generate docs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openpkg",
|
|
@@ -31,11 +31,11 @@
|
|
|
31
31
|
"lint": "biome check src/",
|
|
32
32
|
"lint:fix": "biome check --write src/",
|
|
33
33
|
"format": "biome format --write src/",
|
|
34
|
-
"typecheck": "tsc --noEmit -p .",
|
|
34
|
+
"typecheck": "tsc --noEmit --types bun -p .",
|
|
35
35
|
"test": "bun test"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@openpkg-ts/sdk": "^0.
|
|
38
|
+
"@openpkg-ts/sdk": "^0.52.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/bun": "latest",
|