@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 +135 -0
- package/dist/cli.d.ts +0 -0
- package/dist/cli.js +454 -0
- package/dist/config/index.d.ts +19 -0
- package/dist/config/index.js +110 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# OpenPkg CLI
|
|
2
|
+
|
|
3
|
+
[](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
|
+
}
|