@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.
Files changed (3) hide show
  1. package/README.md +26 -5
  2. package/dist/index.js +137 -27
  3. 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 OpenPkg spec
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 <entry.ts>` | Extract an OpenPkg spec from a TypeScript entry point |
27
- | `docs <entry.ts \| spec.json>` | Generate docs (`-f md\|html\|json`) |
28
- | `list <entry.ts>` | List exports with kind and location (`--json`) |
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
- recommendSemverBump
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 <entry.ts> [-o spec.json] [--follow-external <pkg,...>]
24
- openpkg docs <entry.ts | spec.json> [-f md|html|json] [-o out]
25
- openpkg list <entry.ts> [--json]
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 from a TypeScript entry point
31
- docs Generate docs from an entry point or an existing spec file
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/*"). Default: stub externals.
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 openpkg.config.json:
50
- { "followExternal": ["@acme/payment-kit", "@ai-sdk/*"] }
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-all"] ? true : toList(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 (input.endsWith(".json")) {
151
- docs = createDocs(input);
259
+ if (positionals[0]?.endsWith(".json")) {
260
+ docs = createDocs(positionals[0]);
152
261
  } else {
153
- const { spec, diagnostics } = await extractSpec({ entryFile: input });
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 entryFile = positionals[0];
167
- if (!entryFile)
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.11.3",
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.51.0"
38
+ "@openpkg-ts/sdk": "^0.52.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/bun": "latest",