@jackwener/opencli 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.
Files changed (94) hide show
  1. package/.github/workflows/ci.yml +26 -0
  2. package/.github/workflows/release.yml +40 -0
  3. package/README.md +67 -0
  4. package/SKILL.md +230 -0
  5. package/dist/bilibili.d.ts +13 -0
  6. package/dist/bilibili.js +93 -0
  7. package/dist/browser.d.ts +48 -0
  8. package/dist/browser.js +261 -0
  9. package/dist/clis/bilibili/favorite.d.ts +1 -0
  10. package/dist/clis/bilibili/favorite.js +39 -0
  11. package/dist/clis/bilibili/feed.d.ts +1 -0
  12. package/dist/clis/bilibili/feed.js +64 -0
  13. package/dist/clis/bilibili/history.d.ts +1 -0
  14. package/dist/clis/bilibili/history.js +44 -0
  15. package/dist/clis/bilibili/me.d.ts +1 -0
  16. package/dist/clis/bilibili/me.js +13 -0
  17. package/dist/clis/bilibili/search.d.ts +1 -0
  18. package/dist/clis/bilibili/search.js +24 -0
  19. package/dist/clis/bilibili/user-videos.d.ts +1 -0
  20. package/dist/clis/bilibili/user-videos.js +38 -0
  21. package/dist/clis/github/search.d.ts +1 -0
  22. package/dist/clis/github/search.js +20 -0
  23. package/dist/clis/index.d.ts +13 -0
  24. package/dist/clis/index.js +16 -0
  25. package/dist/clis/zhihu/search.d.ts +1 -0
  26. package/dist/clis/zhihu/search.js +58 -0
  27. package/dist/engine.d.ts +6 -0
  28. package/dist/engine.js +77 -0
  29. package/dist/explore.d.ts +17 -0
  30. package/dist/explore.js +603 -0
  31. package/dist/generate.d.ts +11 -0
  32. package/dist/generate.js +134 -0
  33. package/dist/main.d.ts +5 -0
  34. package/dist/main.js +117 -0
  35. package/dist/output.d.ts +11 -0
  36. package/dist/output.js +98 -0
  37. package/dist/pipeline.d.ts +9 -0
  38. package/dist/pipeline.js +315 -0
  39. package/dist/promote.d.ts +1 -0
  40. package/dist/promote.js +3 -0
  41. package/dist/register.d.ts +2 -0
  42. package/dist/register.js +2 -0
  43. package/dist/registry.d.ts +50 -0
  44. package/dist/registry.js +42 -0
  45. package/dist/runtime.d.ts +12 -0
  46. package/dist/runtime.js +27 -0
  47. package/dist/scaffold.d.ts +2 -0
  48. package/dist/scaffold.js +2 -0
  49. package/dist/smoke.d.ts +2 -0
  50. package/dist/smoke.js +2 -0
  51. package/dist/snapshotFormatter.d.ts +9 -0
  52. package/dist/snapshotFormatter.js +41 -0
  53. package/dist/synthesize.d.ts +10 -0
  54. package/dist/synthesize.js +191 -0
  55. package/dist/validate.d.ts +2 -0
  56. package/dist/validate.js +73 -0
  57. package/dist/verify.d.ts +2 -0
  58. package/dist/verify.js +9 -0
  59. package/package.json +47 -0
  60. package/src/bilibili.ts +111 -0
  61. package/src/browser.ts +260 -0
  62. package/src/clis/bilibili/favorite.ts +42 -0
  63. package/src/clis/bilibili/feed.ts +71 -0
  64. package/src/clis/bilibili/history.ts +48 -0
  65. package/src/clis/bilibili/hot.yaml +38 -0
  66. package/src/clis/bilibili/me.ts +14 -0
  67. package/src/clis/bilibili/search.ts +25 -0
  68. package/src/clis/bilibili/user-videos.ts +42 -0
  69. package/src/clis/github/search.ts +21 -0
  70. package/src/clis/github/trending.yaml +58 -0
  71. package/src/clis/hackernews/top.yaml +36 -0
  72. package/src/clis/index.ts +19 -0
  73. package/src/clis/twitter/trending.yaml +40 -0
  74. package/src/clis/v2ex/hot.yaml +29 -0
  75. package/src/clis/v2ex/latest.yaml +28 -0
  76. package/src/clis/zhihu/hot.yaml +28 -0
  77. package/src/clis/zhihu/search.ts +65 -0
  78. package/src/engine.ts +86 -0
  79. package/src/explore.ts +648 -0
  80. package/src/generate.ts +145 -0
  81. package/src/main.ts +103 -0
  82. package/src/output.ts +96 -0
  83. package/src/pipeline.ts +295 -0
  84. package/src/promote.ts +3 -0
  85. package/src/register.ts +2 -0
  86. package/src/registry.ts +87 -0
  87. package/src/runtime.ts +36 -0
  88. package/src/scaffold.ts +2 -0
  89. package/src/smoke.ts +2 -0
  90. package/src/snapshotFormatter.ts +51 -0
  91. package/src/synthesize.ts +210 -0
  92. package/src/validate.ts +55 -0
  93. package/src/verify.ts +9 -0
  94. package/tsconfig.json +17 -0
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Core registry: Strategy enum, Arg/CliCommand interfaces, cli() registration.
3
+ */
4
+
5
+ export enum Strategy {
6
+ PUBLIC = 'public',
7
+ COOKIE = 'cookie',
8
+ HEADER = 'header',
9
+ INTERCEPT = 'intercept',
10
+ UI = 'ui',
11
+ }
12
+
13
+ export interface Arg {
14
+ name: string;
15
+ type?: string;
16
+ default?: any;
17
+ required?: boolean;
18
+ help?: string;
19
+ choices?: string[];
20
+ }
21
+
22
+ export interface CliCommand {
23
+ site: string;
24
+ name: string;
25
+ description: string;
26
+ domain?: string;
27
+ strategy?: Strategy;
28
+ browser?: boolean;
29
+ args: Arg[];
30
+ columns?: string[];
31
+ func?: (page: any, kwargs: Record<string, any>, debug?: boolean) => Promise<any>;
32
+ pipeline?: any[];
33
+ timeoutSeconds?: number;
34
+ source?: string;
35
+ }
36
+
37
+ export interface CliOptions {
38
+ site: string;
39
+ name: string;
40
+ description?: string;
41
+ domain?: string;
42
+ strategy?: Strategy;
43
+ browser?: boolean;
44
+ args?: Arg[];
45
+ columns?: string[];
46
+ func?: (page: any, kwargs: Record<string, any>, debug?: boolean) => Promise<any>;
47
+ pipeline?: any[];
48
+ timeoutSeconds?: number;
49
+ }
50
+
51
+ const _registry = new Map<string, CliCommand>();
52
+
53
+ export function cli(opts: CliOptions): CliCommand {
54
+ const cmd: CliCommand = {
55
+ site: opts.site,
56
+ name: opts.name,
57
+ description: opts.description ?? '',
58
+ domain: opts.domain,
59
+ strategy: opts.strategy ?? (opts.browser === false ? Strategy.PUBLIC : Strategy.COOKIE),
60
+ browser: opts.browser ?? (opts.strategy === Strategy.PUBLIC ? false : true),
61
+ args: opts.args ?? [],
62
+ columns: opts.columns,
63
+ func: opts.func,
64
+ pipeline: opts.pipeline,
65
+ timeoutSeconds: opts.timeoutSeconds,
66
+ };
67
+
68
+ const key = fullName(cmd);
69
+ _registry.set(key, cmd);
70
+ return cmd;
71
+ }
72
+
73
+ export function getRegistry(): Map<string, CliCommand> {
74
+ return _registry;
75
+ }
76
+
77
+ export function fullName(cmd: CliCommand): string {
78
+ return `${cmd.site}/${cmd.name}`;
79
+ }
80
+
81
+ export function strategyLabel(cmd: CliCommand): string {
82
+ return cmd.strategy ?? 'public';
83
+ }
84
+
85
+ export function registerCommand(cmd: CliCommand): void {
86
+ _registry.set(fullName(cmd), cmd);
87
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Runtime utilities: timeouts and browser session management.
3
+ */
4
+
5
+ export const DEFAULT_BROWSER_CONNECT_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_CONNECT_TIMEOUT ?? '30', 10);
6
+ export const DEFAULT_BROWSER_COMMAND_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_COMMAND_TIMEOUT ?? '45', 10);
7
+ export const DEFAULT_BROWSER_EXPLORE_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_EXPLORE_TIMEOUT ?? '120', 10);
8
+ export const DEFAULT_BROWSER_SMOKE_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_SMOKE_TIMEOUT ?? '60', 10);
9
+
10
+ export async function runWithTimeout<T>(
11
+ promise: Promise<T>,
12
+ opts: { timeout: number; label?: string },
13
+ ): Promise<T> {
14
+ return new Promise<T>((resolve, reject) => {
15
+ const timer = setTimeout(() => {
16
+ reject(new Error(`${opts.label ?? 'Operation'} timed out after ${opts.timeout}s`));
17
+ }, opts.timeout * 1000);
18
+
19
+ promise
20
+ .then((result) => { clearTimeout(timer); resolve(result); })
21
+ .catch((err) => { clearTimeout(timer); reject(err); });
22
+ });
23
+ }
24
+
25
+ export async function browserSession<T>(
26
+ BrowserFactory: new () => any,
27
+ fn: (page: any) => Promise<T>,
28
+ ): Promise<T> {
29
+ const mcp = new BrowserFactory();
30
+ try {
31
+ const page = await mcp.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT });
32
+ return await fn(page);
33
+ } finally {
34
+ await mcp.close().catch(() => {});
35
+ }
36
+ }
@@ -0,0 +1,2 @@
1
+ /** Scaffold a draft CLI from endpoint evidence. */
2
+ export function scaffoldCli(opts: any): any { return { ok: true }; }
package/src/smoke.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** Smoke testing, verification, register, promote, explore, synthesize, scaffold, generate — stubs. */
2
+ export async function runSmoke(cmd: any, page: any, args?: any): Promise<any> { return { ok: true, result: null }; }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Aria snapshot formatter: parses Playwright MCP snapshot text into clean format.
3
+ */
4
+
5
+ export interface FormatOptions {
6
+ interactive?: boolean;
7
+ compact?: boolean;
8
+ maxDepth?: number;
9
+ }
10
+
11
+ export function formatSnapshot(raw: string, opts: FormatOptions = {}): string {
12
+ if (!raw || typeof raw !== 'string') return '';
13
+ const lines = raw.split('\n');
14
+ const result: string[] = [];
15
+ let refCounter = 0;
16
+
17
+ for (const line of lines) {
18
+ if (!line.trim()) continue;
19
+ const indent = line.length - line.trimStart().length;
20
+ const depth = Math.floor(indent / 2);
21
+ if (opts.maxDepth && depth > opts.maxDepth) continue;
22
+
23
+ let content = line.trimStart();
24
+
25
+ // Skip non-interactive elements in interactive mode
26
+ if (opts.interactive) {
27
+ const interactiveRoles = ['button', 'link', 'textbox', 'checkbox', 'radio', 'combobox', 'tab', 'menuitem', 'option'];
28
+ const role = content.split(/[\s[]/)[0]?.toLowerCase() ?? '';
29
+ if (!interactiveRoles.some(r => role.includes(r)) && depth > 1) continue;
30
+ }
31
+
32
+ // Compact: strip verbose role descriptions
33
+ if (opts.compact) {
34
+ content = content
35
+ .replace(/\s*\[.*?\]\s*/g, ' ')
36
+ .replace(/\s+/g, ' ')
37
+ .trim();
38
+ }
39
+
40
+ // Assign refs to interactive elements
41
+ const interactivePattern = /^(button|link|textbox|checkbox|radio|combobox|tab|menuitem|option)\b/i;
42
+ if (interactivePattern.test(content)) {
43
+ refCounter++;
44
+ content = `[@${refCounter}] ${content}`;
45
+ }
46
+
47
+ result.push(' '.repeat(depth) + content);
48
+ }
49
+
50
+ return result.join('\n');
51
+ }
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Synthesize: turn explore capabilities into ready-to-use CLI definitions.
3
+ *
4
+ * Takes the structured capabilities from Deep Explore and generates
5
+ * YAML pipeline files that can be directly registered as CLI commands.
6
+ *
7
+ * This is the bridge between discovery (explore) and usability (CLI).
8
+ */
9
+
10
+ import * as fs from 'node:fs';
11
+ import * as path from 'node:path';
12
+ import yaml from 'js-yaml';
13
+
14
+ export function synthesizeFromExplore(target: string, opts: any = {}): any {
15
+ const exploreDir = fs.existsSync(target) ? target : path.join('.opencli', 'explore', target);
16
+ if (!fs.existsSync(exploreDir)) throw new Error(`Explore dir not found: ${target}`);
17
+
18
+ const manifest = JSON.parse(fs.readFileSync(path.join(exploreDir, 'manifest.json'), 'utf-8'));
19
+ const capabilities = JSON.parse(fs.readFileSync(path.join(exploreDir, 'capabilities.json'), 'utf-8'));
20
+ const endpoints = JSON.parse(fs.readFileSync(path.join(exploreDir, 'endpoints.json'), 'utf-8'));
21
+ const auth = JSON.parse(fs.readFileSync(path.join(exploreDir, 'auth.json'), 'utf-8'));
22
+
23
+ const targetDir = opts.outDir ?? path.join(exploreDir, 'candidates');
24
+ fs.mkdirSync(targetDir, { recursive: true });
25
+
26
+ const site = manifest.site;
27
+ const topN = opts.top ?? 5;
28
+ const candidates: any[] = [];
29
+
30
+ // Sort capabilities by confidence
31
+ const sortedCaps = [...capabilities]
32
+ .sort((a: any, b: any) => (b.confidence ?? 0) - (a.confidence ?? 0))
33
+ .slice(0, topN);
34
+
35
+ for (const cap of sortedCaps) {
36
+ // Find the matching endpoint for more detail
37
+ const endpoint = endpoints.find((ep: any) => ep.pattern === cap.endpoint) ??
38
+ endpoints[0];
39
+
40
+ const candidate = buildCandidateYaml(site, manifest, cap, endpoint);
41
+ const fileName = `${cap.name}.yaml`;
42
+ const filePath = path.join(targetDir, fileName);
43
+ fs.writeFileSync(filePath, yaml.dump(candidate.yaml, { sortKeys: false, lineWidth: 120 }));
44
+
45
+ candidates.push({
46
+ name: cap.name,
47
+ path: filePath,
48
+ strategy: cap.strategy,
49
+ endpoint: cap.endpoint,
50
+ confidence: cap.confidence,
51
+ columns: candidate.yaml.columns,
52
+ });
53
+ }
54
+
55
+ const index = {
56
+ site,
57
+ target_url: manifest.target_url,
58
+ generated_from: exploreDir,
59
+ candidate_count: candidates.length,
60
+ candidates,
61
+ };
62
+ fs.writeFileSync(path.join(targetDir, 'candidates.json'), JSON.stringify(index, null, 2));
63
+
64
+ return {
65
+ site,
66
+ explore_dir: exploreDir,
67
+ out_dir: targetDir,
68
+ candidate_count: candidates.length,
69
+ candidates,
70
+ };
71
+ }
72
+
73
+ /** Volatile params to strip from generated URLs */
74
+ const VOLATILE_PARAMS = new Set(['w_rid', 'wts', 'callback', '_', 'timestamp', 't', 'nonce', 'sign']);
75
+ const SEARCH_PARAM_NAMES = new Set(['q', 'query', 'keyword', 'search', 'wd', 'kw', 'w', 'search_query']);
76
+ const LIMIT_PARAM_NAMES = new Set(['ps', 'page_size', 'limit', 'count', 'per_page', 'size', 'num']);
77
+ const PAGE_PARAM_NAMES = new Set(['pn', 'page', 'page_num', 'offset', 'cursor']);
78
+
79
+ /**
80
+ * Build a clean templated URL from a raw API URL.
81
+ * - Strips volatile params (w_rid, wts, etc.)
82
+ * - Templates search, limit, and pagination params
83
+ * - Builds URL string manually to avoid URL encoding of ${{ }} expressions
84
+ */
85
+ function buildTemplatedUrl(rawUrl: string, cap: any, endpoint: any): string {
86
+ try {
87
+ const u = new URL(rawUrl);
88
+ const base = `${u.protocol}//${u.host}${u.pathname}`;
89
+ const params: Array<[string, string]> = [];
90
+
91
+ const hasKeyword = cap.recommendedArgs?.some((a: any) => a.name === 'keyword');
92
+
93
+ u.searchParams.forEach((v, k) => {
94
+ // Skip volatile params
95
+ if (VOLATILE_PARAMS.has(k)) return;
96
+
97
+ // Template known param types
98
+ if (hasKeyword && SEARCH_PARAM_NAMES.has(k)) {
99
+ params.push([k, '${{ args.keyword }}']);
100
+ } else if (LIMIT_PARAM_NAMES.has(k)) {
101
+ params.push([k, '${{ args.limit | default(20) }}']);
102
+ } else if (PAGE_PARAM_NAMES.has(k)) {
103
+ params.push([k, '${{ args.page | default(1) }}']);
104
+ } else {
105
+ params.push([k, v]);
106
+ }
107
+ });
108
+
109
+ if (params.length === 0) return base;
110
+ return base + '?' + params.map(([k, v]) => `${k}=${v}`).join('&');
111
+ } catch {
112
+ return rawUrl;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Build a YAML pipeline definition from a capability + endpoint.
118
+ */
119
+ function buildCandidateYaml(site: string, manifest: any, cap: any, endpoint: any): { name: string; yaml: any } {
120
+ const needsBrowser = cap.strategy !== 'public';
121
+ const pipeline: any[] = [];
122
+
123
+ // Step 1: Navigate (if browser-based)
124
+ if (needsBrowser) {
125
+ pipeline.push({ navigate: manifest.target_url });
126
+ }
127
+
128
+ // Step 2: Fetch the API — build a clean URL with templates
129
+ const rawUrl = endpoint?.url ?? manifest.target_url;
130
+ const fetchStep: any = { url: buildTemplatedUrl(rawUrl, cap, endpoint) };
131
+
132
+ pipeline.push({ fetch: fetchStep });
133
+
134
+ // Step 3: Select the item path
135
+ if (cap.itemPath) {
136
+ pipeline.push({ select: cap.itemPath });
137
+ }
138
+
139
+ // Step 4: Map fields to columns
140
+ const mapStep: Record<string, string> = {};
141
+ const columns = cap.recommendedColumns ?? ['title', 'url'];
142
+
143
+ // Add a rank column if not doing search
144
+ if (!cap.recommendedArgs?.some((a: any) => a.name === 'keyword')) {
145
+ mapStep['rank'] = '${{ index + 1 }}';
146
+ }
147
+
148
+ // Build field mappings from the endpoint's detected fields
149
+ const detectedFields = endpoint?.detectedFields ?? {};
150
+ for (const col of columns) {
151
+ const fieldPath = detectedFields[col];
152
+ if (fieldPath) {
153
+ mapStep[col] = `\${{ item.${fieldPath} }}`;
154
+ } else {
155
+ mapStep[col] = `\${{ item.${col} }}`;
156
+ }
157
+ }
158
+
159
+ pipeline.push({ map: mapStep });
160
+
161
+ // Step 5: Limit
162
+ pipeline.push({ limit: '${{ args.limit | default(20) }}' });
163
+
164
+ // Build args definition
165
+ const argsDef: Record<string, any> = {};
166
+ for (const arg of cap.recommendedArgs ?? []) {
167
+ const def: any = { type: arg.type ?? 'str' };
168
+ if (arg.required) def.required = true;
169
+ if (arg.default != null) def.default = arg.default;
170
+ if (arg.name === 'keyword') def.description = 'Search keyword';
171
+ else if (arg.name === 'limit') def.description = 'Number of items to return';
172
+ else if (arg.name === 'page') def.description = 'Page number';
173
+ argsDef[arg.name] = def;
174
+ }
175
+
176
+ // Ensure limit arg always exists
177
+ if (!argsDef['limit']) {
178
+ argsDef['limit'] = { type: 'int', default: 20, description: 'Number of items to return' };
179
+ }
180
+
181
+ const allColumns = Object.keys(mapStep);
182
+
183
+ return {
184
+ name: cap.name,
185
+ yaml: {
186
+ site,
187
+ name: cap.name,
188
+ description: `${site} ${cap.name} (auto-generated)`,
189
+ domain: manifest.final_url ? new URL(manifest.final_url).hostname : undefined,
190
+ strategy: cap.strategy,
191
+ browser: needsBrowser,
192
+ args: argsDef,
193
+ pipeline,
194
+ columns: allColumns,
195
+ },
196
+ };
197
+ }
198
+
199
+ export function renderSynthesizeSummary(r: any): string {
200
+ const lines = [
201
+ 'opencli synthesize: OK',
202
+ `Site: ${r.site}`,
203
+ `Source: ${r.explore_dir}`,
204
+ `Candidates: ${r.candidate_count}`,
205
+ ];
206
+ for (const c of r.candidates ?? []) {
207
+ lines.push(` • ${c.name} (${c.strategy}, ${(c.confidence * 100).toFixed(0)}% confidence) → ${c.path}`);
208
+ }
209
+ return lines.join('\n');
210
+ }
@@ -0,0 +1,55 @@
1
+ /** Validate CLI definitions. */
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import yaml from 'js-yaml';
5
+
6
+ export function validateClisWithTarget(dirs: string[], target?: string): any {
7
+ const results: any[] = [];
8
+ let errors = 0; let warnings = 0; let files = 0;
9
+ for (const dir of dirs) {
10
+ if (!fs.existsSync(dir)) continue;
11
+ for (const site of fs.readdirSync(dir)) {
12
+ if (target && site !== target && !target.startsWith(site + '/')) continue;
13
+ const siteDir = path.join(dir, site);
14
+ if (!fs.statSync(siteDir).isDirectory()) continue;
15
+ for (const file of fs.readdirSync(siteDir)) {
16
+ if (!file.endsWith('.yaml') && !file.endsWith('.yml')) continue;
17
+ if (target && target.includes('/') && !target.endsWith(file.replace(/\.(yaml|yml)$/, ''))) continue;
18
+ files++;
19
+ const filePath = path.join(siteDir, file);
20
+ const r = validateYamlFile(filePath);
21
+ results.push(r);
22
+ errors += r.errors.length;
23
+ warnings += r.warnings.length;
24
+ }
25
+ }
26
+ }
27
+ return { ok: errors === 0, results, errors, warnings, files };
28
+ }
29
+
30
+ function validateYamlFile(filePath: string): any {
31
+ const errors: string[] = []; const warnings: string[] = [];
32
+ try {
33
+ const raw = fs.readFileSync(filePath, 'utf-8');
34
+ const def = yaml.load(raw) as any;
35
+ if (!def || typeof def !== 'object') { errors.push('Not a valid YAML object'); return { path: filePath, errors, warnings }; }
36
+ if (!def.site) errors.push('Missing "site"');
37
+ if (!def.name) errors.push('Missing "name"');
38
+ if (def.pipeline && !Array.isArray(def.pipeline)) errors.push('"pipeline" must be an array');
39
+ if (def.columns && !Array.isArray(def.columns)) errors.push('"columns" must be an array');
40
+ if (def.args && typeof def.args !== 'object') errors.push('"args" must be an object');
41
+ } catch (e: any) { errors.push(`YAML parse error: ${e.message}`); }
42
+ return { path: filePath, errors, warnings };
43
+ }
44
+
45
+ export function renderValidationReport(report: any): string {
46
+ const lines = [`opencli validate: ${report.ok ? 'PASS' : 'FAIL'}`, `Checked ${report.results.length} CLI(s) in ${report.files} file(s)`, `Errors: ${report.errors} Warnings: ${report.warnings}`];
47
+ for (const r of report.results) {
48
+ if (r.errors.length > 0 || r.warnings.length > 0) {
49
+ lines.push(`\n${r.path}:`);
50
+ for (const e of r.errors) lines.push(` ❌ ${e}`);
51
+ for (const w of r.warnings) lines.push(` ⚠️ ${w}`);
52
+ }
53
+ }
54
+ return lines.join('\n');
55
+ }
package/src/verify.ts ADDED
@@ -0,0 +1,9 @@
1
+ /** Verification: validate + smoke. */
2
+ import { validateClisWithTarget, renderValidationReport } from './validate.js';
3
+ export async function verifyClis(opts: any): Promise<any> {
4
+ const report = validateClisWithTarget([opts.builtinClis, opts.userClis], opts.target);
5
+ return { ok: report.ok, validation: report, smoke: null };
6
+ }
7
+ export function renderVerifyReport(report: any): string {
8
+ return renderValidationReport(report.validation);
9
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": false,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "declaration": true,
13
+ "incremental": true
14
+ },
15
+ "include": ["src/**/*.ts"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }