@openpkg-ts/cli 0.1.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 ADDED
@@ -0,0 +1,135 @@
1
+ # OpenPkg CLI
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@openpkg-ts%2Fcli.svg)](https://www.npmjs.com/package/@openpkg-ts/cli)
4
+
5
+ Command-line interface for producing OpenPkg specs from TypeScript projects.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ # npm
11
+ npm install -g @openpkg-ts/cli
12
+
13
+ # bun
14
+ bun add -g @openpkg-ts/cli
15
+
16
+ # yarn
17
+ yarn global add @openpkg-ts/cli
18
+
19
+ # pnpm
20
+ pnpm add -g @openpkg-ts/cli
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ```bash
26
+ # Generate openpkg.json for the current package
27
+ openpkg generate
28
+
29
+ # Target a specific entry file
30
+ openpkg generate src/index.ts
31
+
32
+ # Scaffold an OpenPkg config
33
+ openpkg init
34
+ ```
35
+
36
+ `openpkg generate` discovers the package manifest, figures out the correct entry point, resolves external .d.ts files when `node_modules` is present, and writes `openpkg.json` by default.
37
+
38
+ ## Commands
39
+
40
+ ### `openpkg init`
41
+
42
+ Create a starter `openpkg.config` file in the current project. The CLI picks an extension automatically:
43
+
44
+ - `openpkg.config.js` when the nearest `package.json` declares `{ "type": "module" }`
45
+ - `openpkg.config.mjs` otherwise (compatible with both ESM and CommonJS projects)
46
+
47
+ ```bash
48
+ openpkg init --cwd . --format auto
49
+ ```
50
+
51
+ Options:
52
+
53
+ - `--cwd <dir>` – Directory where the config should be created (defaults to current directory).
54
+ - `--format <auto|mjs|js|cjs>` – Override the generated file extension.
55
+
56
+ The command aborts when a config already exists anywhere up the directory tree.
57
+
58
+ ### `openpkg generate [entry]`
59
+
60
+ Generate an OpenPkg spec from a file or package entry point.
61
+
62
+ ```bash
63
+ openpkg generate src/index.ts --output lib/openpkg.json --include=createUser
64
+ ```
65
+
66
+ Key behaviors:
67
+
68
+ - Auto-detects the entry point when `[entry]` is omitted (using `exports`, `main`, or TypeScript config fields).
69
+ - Honors `openpkg.config.*` defaults and then applies CLI flags on top.
70
+ - Emits diagnostics from the TypeScript compiler and from OpenPkg's filtering passes.
71
+ - Writes formatted JSON to `openpkg.json` (or the path supplied via `--output`).
72
+
73
+ #### Options
74
+
75
+ - `[entry]` – Entry file to analyze. Optional when the package exposes a single entry point.
76
+ - `-o, --output <file>` – Output path (default: `openpkg.json`).
77
+ - `-p, --package <name>` – Resolve and analyze a workspace package by name.
78
+ - `--cwd <dir>` – Base directory for resolution (default: current directory).
79
+ - `--no-external-types` – Skip pulling types from `node_modules`.
80
+ - `--include <ids>` – Keep only the listed export identifiers (comma-separated or repeatable).
81
+ - `--exclude <ids>` – Drop the listed export identifiers.
82
+ - `-y, --yes` – Assume "yes" for prompts.
83
+
84
+ ## Configuration File
85
+
86
+ Create an `openpkg.config.ts`, `.js`, or `.mjs` file anywhere above your working directory to keep reusable defaults. Prefer `.mjs`/`.cjs` if you are running the CLI under Node.js without a TypeScript loader.
87
+
88
+ ```ts
89
+ // openpkg.config.mjs
90
+ import { defineConfig } from '@openpkg-ts/cli/config';
91
+
92
+ export default defineConfig({
93
+ include: ['createUser', 'deleteUser'],
94
+ exclude: ['internalHelper'],
95
+ resolveExternalTypes: true,
96
+ });
97
+ ```
98
+
99
+ The CLI searches the current directory and its parents for the first config file and merges those settings with flags provided on the command line. `defineConfig` helps with type-safety but is optional—you can export a plain object as well.
100
+
101
+ ### Supported Options
102
+
103
+ - `include: string[]` – Export identifiers to keep.
104
+ - `exclude: string[]` – Export identifiers to drop.
105
+ - `resolveExternalTypes?: boolean` – Override automatic detection of external type resolution.
106
+
107
+ CLI flags always win over config values. When both provide filters, the CLI prints a short summary of how the sets were combined.
108
+
109
+ ## Filtering Tips
110
+
111
+ - `--include` narrows the spec to the identifiers you care about. Any referenced types that fall outside the allow-list are removed unless they are still referenced.
112
+ - `--exclude` is useful for dropping experimental or internal APIs while keeping everything else.
113
+ - Combine filters in configuration for defaults and override per run via CLI flags.
114
+
115
+ ## Monorepo Support
116
+
117
+ Supply `--package <name>` from the workspace root to locate a child package automatically. The CLI understands npm, pnpm, yarn, and bun workspace layouts.
118
+
119
+ ```bash
120
+ openpkg generate --package @myorg/transactions
121
+ ```
122
+
123
+ ## Output
124
+
125
+ After a successful run the CLI prints:
126
+
127
+ - The relative path to the written spec.
128
+ - Counts for exports and types earned after filtering.
129
+ - Any diagnostics collected during analysis.
130
+
131
+ The JSON schema for the output lives at `schemas/v0.1.0/openpkg.schema.json` in this repository.
132
+
133
+ ## License
134
+
135
+ MIT
package/dist/cli.d.ts ADDED
File without changes
package/dist/cli.js ADDED
@@ -0,0 +1,454 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ OPENPKG_CONFIG_FILENAMES,
4
+ loadOpenPkgConfigInternal
5
+ } from "./config/index.js";
6
+
7
+ // src/cli.ts
8
+ import { readFileSync as readFileSync3 } from "node:fs";
9
+ import * as path4 from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { Command } from "commander";
12
+
13
+ // src/commands/generate.ts
14
+ import * as fs2 from "node:fs";
15
+ import * as path2 from "node:path";
16
+ import chalk2 from "chalk";
17
+ import { OpenPkg } from "@openpkg-ts/sdk";
18
+ import { normalize, validateSpec } from "@openpkg-ts/spec";
19
+ import ora from "ora";
20
+
21
+ // src/utils/filter-options.ts
22
+ import chalk from "chalk";
23
+ var unique = (values) => Array.from(new Set(values));
24
+ var parseListFlag = (value) => {
25
+ if (!value) {
26
+ return;
27
+ }
28
+ const rawItems = Array.isArray(value) ? value : [value];
29
+ const normalized = rawItems.flatMap((item) => String(item).split(",")).map((item) => item.trim()).filter(Boolean);
30
+ return normalized.length > 0 ? unique(normalized) : undefined;
31
+ };
32
+ var formatList = (label, values) => `${label}: ${values.map((value) => chalk.cyan(value)).join(", ")}`;
33
+ var mergeFilterOptions = (config, cliOptions) => {
34
+ const messages = [];
35
+ const configInclude = config?.include;
36
+ const configExclude = config?.exclude;
37
+ const cliInclude = cliOptions.include;
38
+ const cliExclude = cliOptions.exclude;
39
+ let include = configInclude;
40
+ let exclude = configExclude;
41
+ let source = include || exclude ? "config" : undefined;
42
+ if (configInclude) {
43
+ messages.push(formatList("include filters from config", configInclude));
44
+ }
45
+ if (configExclude) {
46
+ messages.push(formatList("exclude filters from config", configExclude));
47
+ }
48
+ if (cliInclude) {
49
+ include = include ? include.filter((item) => cliInclude.includes(item)) : cliInclude;
50
+ source = include ? "combined" : "cli";
51
+ messages.push(formatList("apply include filters from CLI", cliInclude));
52
+ }
53
+ if (cliExclude) {
54
+ exclude = exclude ? unique([...exclude, ...cliExclude]) : cliExclude;
55
+ source = source ? "combined" : "cli";
56
+ messages.push(formatList("apply exclude filters from CLI", cliExclude));
57
+ }
58
+ include = include ? unique(include) : undefined;
59
+ exclude = exclude ? unique(exclude) : undefined;
60
+ if (!include && !exclude) {
61
+ return { messages };
62
+ }
63
+ return {
64
+ include,
65
+ exclude,
66
+ source,
67
+ messages
68
+ };
69
+ };
70
+
71
+ // src/utils/package-utils.ts
72
+ import * as fs from "node:fs";
73
+ import * as path from "node:path";
74
+ async function findEntryPoint(packageDir, preferSource = false) {
75
+ const packageJsonPath = path.join(packageDir, "package.json");
76
+ if (!fs.existsSync(packageJsonPath)) {
77
+ return findDefaultEntryPoint(packageDir);
78
+ }
79
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
80
+ if (preferSource) {
81
+ const srcIndex = path.join(packageDir, "src/index.ts");
82
+ if (fs.existsSync(srcIndex)) {
83
+ return srcIndex;
84
+ }
85
+ }
86
+ if (!preferSource && (packageJson.types || packageJson.typings)) {
87
+ const typesPath = path.join(packageDir, packageJson.types || packageJson.typings);
88
+ if (fs.existsSync(typesPath)) {
89
+ return typesPath;
90
+ }
91
+ }
92
+ if (packageJson.exports) {
93
+ const exportPath = resolveExportsField(packageJson.exports, packageDir);
94
+ if (exportPath) {
95
+ return exportPath;
96
+ }
97
+ }
98
+ if (packageJson.main) {
99
+ const mainBase = packageJson.main.replace(/\.(js|mjs|cjs)$/, "");
100
+ const dtsPath = path.join(packageDir, `${mainBase}.d.ts`);
101
+ if (fs.existsSync(dtsPath)) {
102
+ return dtsPath;
103
+ }
104
+ const tsPath = path.join(packageDir, `${mainBase}.ts`);
105
+ if (fs.existsSync(tsPath)) {
106
+ return tsPath;
107
+ }
108
+ const mainPath = path.join(packageDir, packageJson.main);
109
+ if (fs.existsSync(mainPath) && fs.statSync(mainPath).isDirectory()) {
110
+ const indexDts = path.join(mainPath, "index.d.ts");
111
+ const indexTs = path.join(mainPath, "index.ts");
112
+ if (fs.existsSync(indexDts))
113
+ return indexDts;
114
+ if (fs.existsSync(indexTs))
115
+ return indexTs;
116
+ }
117
+ }
118
+ return findDefaultEntryPoint(packageDir);
119
+ }
120
+ function resolveExportsField(exports, packageDir) {
121
+ if (typeof exports === "string") {
122
+ return findTypeScriptFile(path.join(packageDir, exports));
123
+ }
124
+ if (typeof exports === "object" && exports !== null && "." in exports) {
125
+ const dotExport = exports["."];
126
+ if (typeof dotExport === "string") {
127
+ return findTypeScriptFile(path.join(packageDir, dotExport));
128
+ }
129
+ if (dotExport && typeof dotExport === "object") {
130
+ const dotRecord = dotExport;
131
+ const typesEntry = dotRecord.types;
132
+ if (typeof typesEntry === "string") {
133
+ const typesPath = path.join(packageDir, typesEntry);
134
+ if (fs.existsSync(typesPath)) {
135
+ return typesPath;
136
+ }
137
+ }
138
+ for (const condition of ["import", "require", "default"]) {
139
+ const target = dotRecord[condition];
140
+ if (typeof target === "string") {
141
+ const result = findTypeScriptFile(path.join(packageDir, target));
142
+ if (result)
143
+ return result;
144
+ }
145
+ }
146
+ }
147
+ }
148
+ return null;
149
+ }
150
+ function findTypeScriptFile(jsPath) {
151
+ if (!fs.existsSync(jsPath))
152
+ return null;
153
+ const dtsPath = jsPath.replace(/\.(js|mjs|cjs)$/, ".d.ts");
154
+ if (fs.existsSync(dtsPath)) {
155
+ return dtsPath;
156
+ }
157
+ const tsPath = jsPath.replace(/\.(js|mjs|cjs)$/, ".ts");
158
+ if (fs.existsSync(tsPath)) {
159
+ return tsPath;
160
+ }
161
+ return null;
162
+ }
163
+ async function findDefaultEntryPoint(packageDir) {
164
+ const candidates = [
165
+ "dist/index.d.ts",
166
+ "dist/index.ts",
167
+ "lib/index.d.ts",
168
+ "lib/index.ts",
169
+ "src/index.ts",
170
+ "index.d.ts",
171
+ "index.ts"
172
+ ];
173
+ for (const candidate of candidates) {
174
+ const fullPath = path.join(packageDir, candidate);
175
+ if (fs.existsSync(fullPath)) {
176
+ return fullPath;
177
+ }
178
+ }
179
+ throw new Error(`Could not find entry point in ${packageDir}`);
180
+ }
181
+ async function findPackageInMonorepo(rootDir, packageName) {
182
+ const rootPackageJsonPath = path.join(rootDir, "package.json");
183
+ if (!fs.existsSync(rootPackageJsonPath)) {
184
+ return null;
185
+ }
186
+ const rootPackageJson = JSON.parse(fs.readFileSync(rootPackageJsonPath, "utf-8"));
187
+ const workspacePatterns = Array.isArray(rootPackageJson.workspaces) ? rootPackageJson.workspaces : rootPackageJson.workspaces?.packages || [];
188
+ for (const pattern of workspacePatterns) {
189
+ const searchPath = path.join(rootDir, pattern.replace("/**", "").replace("/*", ""));
190
+ if (fs.existsSync(searchPath) && fs.statSync(searchPath).isDirectory()) {
191
+ const entries = fs.readdirSync(searchPath, { withFileTypes: true });
192
+ for (const entry of entries) {
193
+ if (entry.isDirectory()) {
194
+ const packagePath = path.join(searchPath, entry.name);
195
+ const packageJsonPath = path.join(packagePath, "package.json");
196
+ if (fs.existsSync(packageJsonPath)) {
197
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
198
+ if (packageJson.name === packageName) {
199
+ return packagePath;
200
+ }
201
+ }
202
+ }
203
+ }
204
+ }
205
+ }
206
+ return null;
207
+ }
208
+
209
+ // src/commands/generate.ts
210
+ var defaultDependencies = {
211
+ createOpenPkg: (options) => new OpenPkg(options),
212
+ writeFileSync: fs2.writeFileSync,
213
+ spinner: (text) => ora(text),
214
+ log: console.log,
215
+ error: console.error
216
+ };
217
+ function getArrayLength(value) {
218
+ return Array.isArray(value) ? value.length : 0;
219
+ }
220
+ function registerGenerateCommand(program, dependencies = {}) {
221
+ const { createOpenPkg, writeFileSync: writeFileSync2, spinner, log, error } = {
222
+ ...defaultDependencies,
223
+ ...dependencies
224
+ };
225
+ program.command("generate [entry]").description("Generate OpenPkg specification").option("-o, --output <file>", "Output file", "openpkg.json").option("-p, --package <name>", "Target package name (for monorepos)").option("--cwd <dir>", "Working directory", process.cwd()).option("--no-external-types", "Skip external type resolution from node_modules").option("--include <ids>", "Filter exports by identifier (comma-separated or repeated)").option("--exclude <ids>", "Exclude exports by identifier (comma-separated or repeated)").option("-y, --yes", "Skip all prompts and use defaults").action(async (entry, options) => {
226
+ try {
227
+ let targetDir = options.cwd;
228
+ let entryFile = entry;
229
+ if (options.package) {
230
+ const packageDir = await findPackageInMonorepo(options.cwd, options.package);
231
+ if (!packageDir) {
232
+ throw new Error(`Package "${options.package}" not found in monorepo`);
233
+ }
234
+ targetDir = packageDir;
235
+ log(chalk2.gray(`Found package at ${path2.relative(options.cwd, packageDir)}`));
236
+ }
237
+ if (!entryFile) {
238
+ entryFile = await findEntryPoint(targetDir, true);
239
+ log(chalk2.gray(`Auto-detected entry point: ${path2.relative(targetDir, entryFile)}`));
240
+ } else {
241
+ entryFile = path2.resolve(targetDir, entryFile);
242
+ }
243
+ const resolveExternalTypes = options.externalTypes !== false;
244
+ const cliFilters = {
245
+ include: parseListFlag(options.include),
246
+ exclude: parseListFlag(options.exclude)
247
+ };
248
+ let config = null;
249
+ try {
250
+ config = await loadOpenPkgConfigInternal(targetDir);
251
+ if (config?.filePath) {
252
+ log(chalk2.gray(`Loaded configuration from ${path2.relative(targetDir, config.filePath)}`));
253
+ }
254
+ } catch (configError) {
255
+ error(chalk2.red("Failed to load OpenPkg config:"), configError instanceof Error ? configError.message : configError);
256
+ process.exit(1);
257
+ }
258
+ const resolvedFilters = mergeFilterOptions(config, cliFilters);
259
+ for (const message of resolvedFilters.messages) {
260
+ log(chalk2.gray(`• ${message}`));
261
+ }
262
+ const spinnerInstance = spinner("Generating OpenPkg spec...");
263
+ spinnerInstance.start();
264
+ let result;
265
+ try {
266
+ const openpkg = createOpenPkg({
267
+ resolveExternalTypes
268
+ });
269
+ const analyzeOptions = resolvedFilters.include || resolvedFilters.exclude ? {
270
+ filters: {
271
+ include: resolvedFilters.include,
272
+ exclude: resolvedFilters.exclude
273
+ }
274
+ } : {};
275
+ result = await openpkg.analyzeFileWithDiagnostics(entryFile, analyzeOptions);
276
+ spinnerInstance.succeed("Generated OpenPkg spec");
277
+ } catch (generationError) {
278
+ spinnerInstance.fail("Failed to generate spec");
279
+ throw generationError;
280
+ }
281
+ if (!result) {
282
+ throw new Error("Failed to produce an OpenPkg spec.");
283
+ }
284
+ const outputPath = path2.resolve(targetDir, options.output);
285
+ const normalized = normalize(result.spec);
286
+ const validation = validateSpec(normalized);
287
+ if (!validation.ok) {
288
+ spinnerInstance.fail("Spec failed schema validation");
289
+ for (const err of validation.errors) {
290
+ error(chalk2.red(`schema: ${err.instancePath || "/"} ${err.message}`));
291
+ }
292
+ process.exit(1);
293
+ }
294
+ writeFileSync2(outputPath, JSON.stringify(normalized, null, 2));
295
+ log(chalk2.green(`✓ Generated ${path2.relative(process.cwd(), outputPath)}`));
296
+ log(chalk2.gray(` ${getArrayLength(normalized.exports)} exports`));
297
+ log(chalk2.gray(` ${getArrayLength(normalized.types)} types`));
298
+ if (result.diagnostics.length > 0) {
299
+ log("");
300
+ log(chalk2.bold("Diagnostics"));
301
+ for (const diagnostic of result.diagnostics) {
302
+ const prefix = diagnostic.severity === "error" ? chalk2.red("✖") : diagnostic.severity === "warning" ? chalk2.yellow("⚠") : chalk2.cyan("ℹ");
303
+ log(`${prefix} ${diagnostic.message}`);
304
+ }
305
+ }
306
+ } catch (commandError) {
307
+ error(chalk2.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
308
+ process.exit(1);
309
+ }
310
+ });
311
+ }
312
+
313
+ // src/commands/init.ts
314
+ import * as fs3 from "node:fs";
315
+ import * as path3 from "node:path";
316
+ import chalk3 from "chalk";
317
+ var defaultDependencies2 = {
318
+ fileExists: fs3.existsSync,
319
+ writeFileSync: fs3.writeFileSync,
320
+ readFileSync: fs3.readFileSync,
321
+ log: console.log,
322
+ error: console.error
323
+ };
324
+ function registerInitCommand(program, dependencies = {}) {
325
+ const { fileExists, writeFileSync: writeFileSync3, readFileSync: readFileSync3, log, error } = {
326
+ ...defaultDependencies2,
327
+ ...dependencies
328
+ };
329
+ program.command("init").description("Create an OpenPkg configuration file").option("--cwd <dir>", "Working directory", process.cwd()).option("--format <format>", "Config format: auto, mjs, js, cjs", "auto").action((options) => {
330
+ const cwd = path3.resolve(options.cwd);
331
+ const formatOption = String(options.format ?? "auto").toLowerCase();
332
+ if (!isValidFormat(formatOption)) {
333
+ error(chalk3.red(`Invalid format "${formatOption}". Use auto, mjs, js, or cjs.`));
334
+ process.exitCode = 1;
335
+ return;
336
+ }
337
+ const existing = findExistingConfig(cwd, fileExists);
338
+ if (existing) {
339
+ error(chalk3.red(`An OpenPkg config already exists at ${path3.relative(cwd, existing) || "./openpkg.config.*"}.`));
340
+ process.exitCode = 1;
341
+ return;
342
+ }
343
+ const packageType = detectPackageType(cwd, fileExists, readFileSync3);
344
+ const targetFormat = resolveFormat(formatOption, packageType);
345
+ if (targetFormat === "js" && packageType !== "module") {
346
+ log(chalk3.yellow('Package is not marked as "type": "module"; creating openpkg.config.js may require enabling ESM.'));
347
+ }
348
+ const fileName = `openpkg.config.${targetFormat}`;
349
+ const outputPath = path3.join(cwd, fileName);
350
+ if (fileExists(outputPath)) {
351
+ error(chalk3.red(`Cannot create ${fileName}; file already exists.`));
352
+ process.exitCode = 1;
353
+ return;
354
+ }
355
+ const template = buildTemplate(targetFormat);
356
+ writeFileSync3(outputPath, template, { encoding: "utf8" });
357
+ log(chalk3.green(`✓ Created ${path3.relative(process.cwd(), outputPath)}`));
358
+ });
359
+ }
360
+ var isValidFormat = (value) => {
361
+ return value === "auto" || value === "mjs" || value === "js" || value === "cjs";
362
+ };
363
+ var findExistingConfig = (cwd, fileExists) => {
364
+ let current = path3.resolve(cwd);
365
+ const { root } = path3.parse(current);
366
+ while (true) {
367
+ for (const candidate of OPENPKG_CONFIG_FILENAMES) {
368
+ const candidatePath = path3.join(current, candidate);
369
+ if (fileExists(candidatePath)) {
370
+ return candidatePath;
371
+ }
372
+ }
373
+ if (current === root) {
374
+ break;
375
+ }
376
+ current = path3.dirname(current);
377
+ }
378
+ return null;
379
+ };
380
+ var detectPackageType = (cwd, fileExists, readFileSync3) => {
381
+ const packageJsonPath = findNearestPackageJson(cwd, fileExists);
382
+ if (!packageJsonPath) {
383
+ return;
384
+ }
385
+ try {
386
+ const raw = readFileSync3(packageJsonPath, "utf8");
387
+ const parsed = JSON.parse(raw);
388
+ if (parsed.type === "module") {
389
+ return "module";
390
+ }
391
+ if (parsed.type === "commonjs") {
392
+ return "commonjs";
393
+ }
394
+ } catch (_error) {}
395
+ return;
396
+ };
397
+ var findNearestPackageJson = (cwd, fileExists) => {
398
+ let current = path3.resolve(cwd);
399
+ const { root } = path3.parse(current);
400
+ while (true) {
401
+ const candidate = path3.join(current, "package.json");
402
+ if (fileExists(candidate)) {
403
+ return candidate;
404
+ }
405
+ if (current === root) {
406
+ break;
407
+ }
408
+ current = path3.dirname(current);
409
+ }
410
+ return null;
411
+ };
412
+ var resolveFormat = (format, packageType) => {
413
+ if (format === "auto") {
414
+ return packageType === "module" ? "js" : "mjs";
415
+ }
416
+ return format;
417
+ };
418
+ var buildTemplate = (format) => {
419
+ if (format === "cjs") {
420
+ return [
421
+ "const { defineConfig } = require('@openpkg-ts/cli/config');",
422
+ "",
423
+ "module.exports = defineConfig({",
424
+ " include: [],",
425
+ " exclude: [],",
426
+ "});",
427
+ ""
428
+ ].join(`
429
+ `);
430
+ }
431
+ return [
432
+ "import { defineConfig } from '@openpkg-ts/cli/config';",
433
+ "",
434
+ "export default defineConfig({",
435
+ " include: [],",
436
+ " exclude: [],",
437
+ "});",
438
+ ""
439
+ ].join(`
440
+ `);
441
+ };
442
+
443
+ // src/cli.ts
444
+ var __filename2 = fileURLToPath(import.meta.url);
445
+ var __dirname2 = path4.dirname(__filename2);
446
+ var packageJson = JSON.parse(readFileSync3(path4.join(__dirname2, "../package.json"), "utf-8"));
447
+ var program = new Command;
448
+ program.name("openpkg").description("Generate OpenPkg specification for TypeScript packages").version(packageJson.version);
449
+ registerGenerateCommand(program);
450
+ registerInitCommand(program);
451
+ program.command("*", { hidden: true }).action(() => {
452
+ program.outputHelp();
453
+ });
454
+ program.parseAsync();
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ declare const stringList: z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>;
3
+ declare const openPkgConfigSchema: z.ZodObject<{
4
+ include: z.ZodOptional<typeof stringList>
5
+ exclude: z.ZodOptional<typeof stringList>
6
+ plugins: z.ZodOptional<z.ZodArray<z.ZodUnknown, "many">>
7
+ }>;
8
+ type OpenPkgConfigInput = z.infer<typeof openPkgConfigSchema>;
9
+ interface NormalizedOpenPkgConfig {
10
+ include?: string[];
11
+ exclude?: string[];
12
+ plugins?: unknown[];
13
+ }
14
+ interface LoadedOpenPkgConfig extends NormalizedOpenPkgConfig {
15
+ filePath: string;
16
+ }
17
+ declare const loadOpenPkgConfigInternal: (cwd: string) => Promise<LoadedOpenPkgConfig | null>;
18
+ declare const define: (config: OpenPkgConfigInput) => OpenPkgConfigInput;
19
+ export { loadOpenPkgConfigInternal as loadOpenPkgConfig, define as defineConfig, NormalizedOpenPkgConfig, LoadedOpenPkgConfig };
@@ -0,0 +1,110 @@
1
+ // src/config/openpkg-config.ts
2
+ import { access } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+
6
+ // src/config/schema.ts
7
+ import { z } from "zod";
8
+ var stringList = z.union([
9
+ z.string(),
10
+ z.array(z.string())
11
+ ]);
12
+ var openPkgConfigSchema = z.object({
13
+ include: stringList.optional(),
14
+ exclude: stringList.optional(),
15
+ plugins: z.array(z.unknown()).optional()
16
+ });
17
+ var normalizeList = (value) => {
18
+ if (!value) {
19
+ return;
20
+ }
21
+ const list = Array.isArray(value) ? value : [value];
22
+ const normalized = list.map((item) => item.trim()).filter(Boolean);
23
+ return normalized.length > 0 ? normalized : undefined;
24
+ };
25
+ var normalizeConfig = (input) => {
26
+ const include = normalizeList(input.include);
27
+ const exclude = normalizeList(input.exclude);
28
+ return {
29
+ include,
30
+ exclude,
31
+ plugins: input.plugins
32
+ };
33
+ };
34
+
35
+ // src/config/openpkg-config.ts
36
+ var OPENPKG_CONFIG_FILENAMES = [
37
+ "openpkg.config.ts",
38
+ "openpkg.config.mts",
39
+ "openpkg.config.cts",
40
+ "openpkg.config.js",
41
+ "openpkg.config.mjs",
42
+ "openpkg.config.cjs"
43
+ ];
44
+ var fileExists = async (filePath) => {
45
+ try {
46
+ await access(filePath);
47
+ return true;
48
+ } catch {
49
+ return false;
50
+ }
51
+ };
52
+ var findConfigFile = async (cwd) => {
53
+ let current = path.resolve(cwd);
54
+ const { root } = path.parse(current);
55
+ while (true) {
56
+ for (const candidate of OPENPKG_CONFIG_FILENAMES) {
57
+ const candidatePath = path.join(current, candidate);
58
+ if (await fileExists(candidatePath)) {
59
+ return candidatePath;
60
+ }
61
+ }
62
+ if (current === root) {
63
+ return null;
64
+ }
65
+ current = path.dirname(current);
66
+ }
67
+ };
68
+ var importConfigModule = async (absolutePath) => {
69
+ const fileUrl = pathToFileURL(absolutePath);
70
+ fileUrl.searchParams.set("t", Date.now().toString());
71
+ const module = await import(fileUrl.href);
72
+ return module?.default ?? module?.config ?? module;
73
+ };
74
+ var formatIssues = (issues) => issues.map((issue) => `- ${issue}`).join(`
75
+ `);
76
+ var loadOpenPkgConfigInternal = async (cwd) => {
77
+ const configPath = await findConfigFile(cwd);
78
+ if (!configPath) {
79
+ return null;
80
+ }
81
+ let rawConfig;
82
+ try {
83
+ rawConfig = await importConfigModule(configPath);
84
+ } catch (error) {
85
+ const message = error instanceof Error ? error.message : String(error);
86
+ throw new Error(`Failed to load OpenPkg config at ${configPath}: ${message}`);
87
+ }
88
+ const parsed = openPkgConfigSchema.safeParse(rawConfig);
89
+ if (!parsed.success) {
90
+ const issues = parsed.error.issues.map((issue) => {
91
+ const pathLabel = issue.path.length > 0 ? issue.path.join(".") : "(root)";
92
+ return `${pathLabel}: ${issue.message}`;
93
+ });
94
+ throw new Error(`Invalid OpenPkg configuration at ${configPath}.
95
+ ${formatIssues(issues)}`);
96
+ }
97
+ const normalized = normalizeConfig(parsed.data);
98
+ return {
99
+ filePath: configPath,
100
+ ...normalized
101
+ };
102
+ };
103
+
104
+ // src/config/index.ts
105
+ var define = (config) => config;
106
+ export {
107
+ loadOpenPkgConfigInternal as loadOpenPkgConfig,
108
+ define as defineConfig
109
+ };
110
+ export { OPENPKG_CONFIG_FILENAMES, loadOpenPkgConfigInternal };
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@openpkg-ts/cli",
3
+ "version": "0.1.0",
4
+ "description": "OpenAPI-like specification generator for TypeScript packages",
5
+ "keywords": [
6
+ "typescript",
7
+ "cli",
8
+ "documentation",
9
+ "openpkg",
10
+ "package-analysis",
11
+ "openapi"
12
+ ],
13
+ "homepage": "https://github.com/openpkg/openpkg#readme",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/openpkg/openpkg.git",
17
+ "directory": "packages/cli"
18
+ },
19
+ "license": "MIT",
20
+ "author": "Ryan Waits",
21
+ "type": "module",
22
+ "main": "./dist/index.js",
23
+ "types": "./dist/index.d.ts",
24
+ "bin": {
25
+ "openpkg": "./dist/cli.js"
26
+ },
27
+ "exports": {
28
+ ".": {
29
+ "import": "./dist/index.js",
30
+ "types": "./dist/index.d.ts"
31
+ },
32
+ "./config": {
33
+ "import": "./dist/config/index.js",
34
+ "types": "./dist/config/index.d.ts"
35
+ }
36
+ },
37
+ "scripts": {
38
+ "build": "bunup",
39
+ "dev": "bunup --watch",
40
+ "cli": "bun run src/cli.ts",
41
+ "lint": "biome check src/",
42
+ "lint:fix": "biome check --write src/",
43
+ "format": "biome format --write src/"
44
+ },
45
+ "files": [
46
+ "dist"
47
+ ],
48
+ "dependencies": {
49
+ "@inquirer/prompts": "^7.8.0",
50
+ "@openpkg-ts/sdk": "^0.1.0",
51
+ "@openpkg-ts/spec": "^0.1.0",
52
+ "chalk": "^5.4.1",
53
+ "commander": "^14.0.0",
54
+ "ora": "^8.2.0",
55
+ "zod": "^4.0.5"
56
+ },
57
+ "devDependencies": {
58
+ "@types/bun": "latest",
59
+ "@types/node": "^20.0.0",
60
+ "bunup": "latest"
61
+ },
62
+ "publishConfig": {
63
+ "access": "public"
64
+ }
65
+ }