@openpkg-ts/cli 0.12.0 → 0.12.1

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 (2) hide show
  1. package/dist/index.js +200 -99
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import fs from "node:fs";
5
- import path from "node:path";
4
+ import fs2 from "node:fs";
5
+ import path2 from "node:path";
6
6
  import readline from "node:readline/promises";
7
7
  import { parseArgs } from "node:util";
8
8
  import {
@@ -13,6 +13,7 @@ import {
13
13
  extractSpec,
14
14
  getAvailableVersions,
15
15
  getValidationErrors,
16
+ isPathLikeInput,
16
17
  isRemoteInput,
17
18
  listExports,
18
19
  loadConfig,
@@ -21,6 +22,78 @@ import {
21
22
  recommendSemverBump,
22
23
  resolveTarget
23
24
  } from "@openpkg-ts/sdk";
25
+
26
+ // src/env.ts
27
+ import fs from "node:fs";
28
+ import path from "node:path";
29
+ function parseEnvText(content) {
30
+ const out = {};
31
+ for (const rawLine of content.split(/\r?\n/)) {
32
+ let line = rawLine.trim();
33
+ if (!line || line.startsWith("#"))
34
+ continue;
35
+ if (line.startsWith("export "))
36
+ line = line.slice(7).trimStart();
37
+ const eq = line.indexOf("=");
38
+ if (eq <= 0)
39
+ continue;
40
+ const key = line.slice(0, eq).trim();
41
+ if (!key || /\s/.test(key))
42
+ continue;
43
+ let value = line.slice(eq + 1);
44
+ if (value.startsWith(" ") || value.startsWith("\t"))
45
+ value = value.trimStart();
46
+ if (value.startsWith('"')) {
47
+ let i = 1;
48
+ let parsed = "";
49
+ while (i < value.length) {
50
+ const ch = value[i];
51
+ if (ch === "\\" && i + 1 < value.length) {
52
+ const next = value[i + 1];
53
+ parsed += next === "n" ? `
54
+ ` : next === "r" ? "\r" : next === "t" ? "\t" : next;
55
+ i += 2;
56
+ continue;
57
+ }
58
+ if (ch === '"')
59
+ break;
60
+ parsed += ch;
61
+ i++;
62
+ }
63
+ out[key] = parsed;
64
+ continue;
65
+ }
66
+ if (value.startsWith("'")) {
67
+ const end = value.indexOf("'", 1);
68
+ out[key] = end === -1 ? value.slice(1) : value.slice(1, end);
69
+ continue;
70
+ }
71
+ const hash = value.search(/\s+#/);
72
+ out[key] = (hash === -1 ? value : value.slice(0, hash)).trim();
73
+ }
74
+ return out;
75
+ }
76
+ function parseIfExists(file) {
77
+ try {
78
+ if (!fs.existsSync(file))
79
+ return {};
80
+ return parseEnvText(fs.readFileSync(file, "utf8"));
81
+ } catch {
82
+ return {};
83
+ }
84
+ }
85
+ function loadCwdEnv(cwd = process.cwd()) {
86
+ const parsed = {
87
+ ...parseIfExists(path.join(cwd, ".env")),
88
+ ...parseIfExists(path.join(cwd, ".env.local"))
89
+ };
90
+ for (const [k, v] of Object.entries(parsed)) {
91
+ if (process.env[k] === undefined)
92
+ process.env[k] = v;
93
+ }
94
+ }
95
+
96
+ // src/index.ts
24
97
  var HELP = `openpkg - extract TypeScript API specs and generate docs
25
98
 
26
99
  Usage:
@@ -56,21 +129,31 @@ cwd. Flags override the file. Example:
56
129
  { "followExternal": ["@ai-sdk/*"] }
57
130
  { "followExternal": "auto", "decisions": "jev" }
58
131
  `;
59
- function fail(message) {
60
- console.error(`error: ${message}`);
61
- process.exit(1);
132
+
133
+ class CliError extends Error {
134
+ exitCode;
135
+ printed;
136
+ constructor(message, exitCode = 1, printed = false) {
137
+ super(message);
138
+ this.exitCode = exitCode;
139
+ this.printed = printed;
140
+ this.name = "CliError";
141
+ }
142
+ }
143
+ function fail(message, exitCode = 1) {
144
+ throw new CliError(message, exitCode);
62
145
  }
63
146
  function write(content, output) {
64
147
  if (output) {
65
- fs.mkdirSync(path.dirname(path.resolve(output)), { recursive: true });
66
- fs.writeFileSync(output, content);
148
+ fs2.mkdirSync(path2.dirname(path2.resolve(output)), { recursive: true });
149
+ fs2.writeFileSync(output, content);
67
150
  console.error(`wrote ${output}`);
68
151
  } else {
69
152
  console.log(content);
70
153
  }
71
154
  }
72
155
  function version() {
73
- const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
156
+ const pkg = JSON.parse(fs2.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
74
157
  return pkg.version;
75
158
  }
76
159
  function reportDiagnostics(diagnostics) {
@@ -80,7 +163,7 @@ function reportDiagnostics(diagnostics) {
80
163
  }
81
164
  }
82
165
  if (diagnostics.some((d) => d.severity === "error")) {
83
- process.exit(1);
166
+ throw new CliError("", 1, true);
84
167
  }
85
168
  }
86
169
  function parseTargetArgs(positionals, cwd) {
@@ -88,18 +171,18 @@ function parseTargetArgs(positionals, cwd) {
88
171
  return { input: cwd };
89
172
  const first = positionals[0];
90
173
  const rest = positionals.slice(1).join(" ").trim();
91
- const abs = path.resolve(cwd, first);
92
174
  if (isRemoteInput(first)) {
93
175
  return { input: first, ...rest ? { intent: rest } : {} };
94
176
  }
95
- if (fs.existsSync(abs)) {
177
+ const abs = path2.resolve(cwd, first);
178
+ if (fs2.existsSync(abs) || isPathLikeInput(first)) {
96
179
  return { input: abs, ...rest ? { intent: rest } : {} };
97
180
  }
98
181
  return { input: cwd, intent: positionals.join(" ") };
99
182
  }
100
183
  function formatPackages(candidates, cwd) {
101
184
  return candidates.map((c, i) => {
102
- const rel = path.relative(cwd, c.dir) || ".";
185
+ const rel = path2.relative(cwd, c.dir) || ".";
103
186
  return ` ${i + 1}. ${c.name} ${rel}`;
104
187
  }).join(`
105
188
  `);
@@ -124,54 +207,44 @@ re-run with a path or intent, e.g. openpkg spec . ${hint}`);
124
207
  rl.close();
125
208
  }
126
209
  }
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
210
  async function resolveCliTarget(positionals, decisions) {
148
211
  loadCwdEnv();
149
212
  const cwd = process.cwd();
150
213
  const { input, intent } = parseTargetArgs(positionals, cwd);
151
214
  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 };
215
+ const cleanup = resolved.cleanup;
216
+ try {
217
+ if (resolved.kind === "unavailable")
218
+ fail(resolved.reason);
219
+ if (resolved.kind === "empty")
220
+ fail(resolved.reason);
221
+ if (resolved.kind === "needs-build") {
222
+ fail(resolved.command ? `${resolved.reason}
223
+ ${resolved.command}` : resolved.reason, 2);
224
+ }
225
+ if (resolved.kind === "explicit") {
226
+ return {
227
+ entryFile: resolved.entryFile,
228
+ entryPointSource: resolved.entryPointSource,
229
+ cleanup
230
+ };
231
+ }
232
+ if (resolved.kind === "ok") {
233
+ return {
234
+ entryFile: resolved.entryFile,
235
+ entryPointSource: resolved.entryPointSource,
236
+ cleanup
237
+ };
238
+ }
239
+ const chosen = await choosePackage(resolved.candidates, cwd);
240
+ const picked = pickEntry(chosen.dir);
241
+ if (!picked)
242
+ fail(`no TypeScript entry found in ${chosen.name}`);
243
+ return { ...picked, cleanup };
244
+ } catch (err) {
245
+ cleanup?.();
246
+ throw err;
169
247
  }
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
248
  }
176
249
  function toList(value) {
177
250
  if (!value)
@@ -223,24 +296,31 @@ async function specCommand(args) {
223
296
  ignore: toList(values.ignore),
224
297
  ...values.jev ? { decisions: "jev" } : {}
225
298
  };
226
- const { entryFile, entryPointSource } = await resolveCliTarget(positionals, cliConfig.decisions ?? fileConfig?.decisions);
227
- const config = mergeConfig(fileConfig, cliConfig);
228
- if (config.followExternal === "auto" && config.decisions !== "jev") {
229
- fail("followExternal auto requires --jev");
299
+ let cleanup;
300
+ try {
301
+ const resolved = await resolveCliTarget(positionals, cliConfig.decisions ?? fileConfig?.decisions);
302
+ cleanup = resolved.cleanup;
303
+ const { entryFile, entryPointSource } = resolved;
304
+ const config = mergeConfig(fileConfig, cliConfig);
305
+ if (config.followExternal === "auto" && config.decisions !== "jev") {
306
+ fail("followExternal auto requires --jev");
307
+ }
308
+ const { spec, diagnostics } = await extractSpec({
309
+ entryFile,
310
+ entryPointSource,
311
+ followExternal: config.followExternal,
312
+ only: config.only,
313
+ ignore: config.ignore,
314
+ externals: config.externals,
315
+ decisions: config.decisions
316
+ });
317
+ reportDiagnostics(diagnostics);
318
+ if (!config.followExternal)
319
+ reportStubbedExternals(spec);
320
+ write(JSON.stringify(spec, null, 2), values.output);
321
+ } finally {
322
+ cleanup?.();
230
323
  }
231
- const { spec, diagnostics } = await extractSpec({
232
- entryFile,
233
- entryPointSource,
234
- followExternal: config.followExternal,
235
- only: config.only,
236
- ignore: config.ignore,
237
- externals: config.externals,
238
- decisions: config.decisions
239
- });
240
- reportDiagnostics(diagnostics);
241
- if (!config.followExternal)
242
- reportStubbedExternals(spec);
243
- write(JSON.stringify(spec, null, 2), values.output);
244
324
  }
245
325
  async function docsCommand(args) {
246
326
  const { values, positionals } = parseArgs({
@@ -256,17 +336,26 @@ async function docsCommand(args) {
256
336
  if (!["md", "html", "json"].includes(format))
257
337
  fail(`unknown format "${format}" (md|html|json)`);
258
338
  let docs;
259
- if (positionals[0]?.endsWith(".json")) {
260
- docs = createDocs(positionals[0]);
261
- } else {
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 });
265
- reportDiagnostics(diagnostics);
266
- docs = createDocs(spec);
339
+ let cleanup;
340
+ try {
341
+ if (positionals[0]?.endsWith(".json")) {
342
+ docs = createDocs(positionals[0]);
343
+ } else {
344
+ const decisions = values.jev ? "jev" : loadConfig(process.cwd())?.decisions;
345
+ const resolved = await resolveCliTarget(positionals, decisions);
346
+ cleanup = resolved.cleanup;
347
+ const { spec, diagnostics } = await extractSpec({
348
+ entryFile: resolved.entryFile,
349
+ entryPointSource: resolved.entryPointSource
350
+ });
351
+ reportDiagnostics(diagnostics);
352
+ docs = createDocs(spec);
353
+ }
354
+ const content = format === "md" ? docs.toMarkdown() : format === "html" ? docs.toHTML() : JSON.stringify(docs.toJSON(), null, 2);
355
+ write(content, values.output);
356
+ } finally {
357
+ cleanup?.();
267
358
  }
268
- const content = format === "md" ? docs.toMarkdown() : format === "html" ? docs.toHTML() : JSON.stringify(docs.toJSON(), null, 2);
269
- write(content, values.output);
270
359
  }
271
360
  async function listCommand(args) {
272
361
  const { values, positionals } = parseArgs({
@@ -275,26 +364,32 @@ async function listCommand(args) {
275
364
  allowPositionals: true
276
365
  });
277
366
  const decisions = values.jev ? "jev" : loadConfig(process.cwd())?.decisions;
278
- const { entryFile } = await resolveCliTarget(positionals, decisions);
279
- const { exports, errors } = await listExports({ entryFile });
280
- for (const err of errors) {
281
- console.error(`error: ${err}`);
282
- }
283
- if (errors.length > 0 && exports.length === 0) {
284
- process.exit(1);
285
- }
286
- if (values.json) {
287
- console.log(JSON.stringify(exports, null, 2));
288
- return;
289
- }
290
- for (const exp of exports) {
291
- const location = exp.file ? ` (${exp.file}:${exp.line})` : "";
292
- console.log(`${exp.kind.padEnd(10)}${exp.name}${location}`);
367
+ let cleanup;
368
+ try {
369
+ const resolved = await resolveCliTarget(positionals, decisions);
370
+ cleanup = resolved.cleanup;
371
+ const { exports, errors } = await listExports({ entryFile: resolved.entryFile });
372
+ for (const err of errors) {
373
+ console.error(`error: ${err}`);
374
+ }
375
+ if (errors.length > 0 && exports.length === 0) {
376
+ throw new CliError("", 1, true);
377
+ }
378
+ if (values.json) {
379
+ console.log(JSON.stringify(exports, null, 2));
380
+ return;
381
+ }
382
+ for (const exp of exports) {
383
+ const location = exp.file ? ` (${exp.file}:${exp.line})` : "";
384
+ console.log(`${exp.kind.padEnd(10)}${exp.name}${location}`);
385
+ }
386
+ } finally {
387
+ cleanup?.();
293
388
  }
294
389
  }
295
390
  function readSpecFile(file) {
296
391
  try {
297
- return JSON.parse(fs.readFileSync(file, "utf-8"));
392
+ return JSON.parse(fs2.readFileSync(file, "utf-8"));
298
393
  } catch (err) {
299
394
  fail(`failed to read spec file ${file}: ${err instanceof Error ? err.message : String(err)}`);
300
395
  }
@@ -330,7 +425,7 @@ function validateCommand(args) {
330
425
  for (const e of errors) {
331
426
  console.error(`${e.instancePath || "/"} ${e.message}`);
332
427
  }
333
- process.exit(1);
428
+ throw new CliError("", 1, true);
334
429
  }
335
430
  function diffCommand(args) {
336
431
  const { values, positionals } = parseArgs({
@@ -412,5 +507,11 @@ async function main() {
412
507
  }
413
508
  }
414
509
  main().catch((err) => {
415
- fail(err instanceof Error ? err.message : String(err));
510
+ if (err instanceof CliError) {
511
+ if (!err.printed && err.message)
512
+ console.error(`error: ${err.message}`);
513
+ process.exit(err.exitCode);
514
+ }
515
+ console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
516
+ process.exit(1);
416
517
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpkg-ts/cli",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "CLI for OpenPkg - extract TypeScript API specs and generate docs",
5
5
  "keywords": [
6
6
  "openpkg",
@@ -35,7 +35,7 @@
35
35
  "test": "bun test"
36
36
  },
37
37
  "dependencies": {
38
- "@openpkg-ts/sdk": "^0.52.0"
38
+ "@openpkg-ts/sdk": "^0.52.2"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/bun": "latest",