@leeguoo/yapi-mcp 0.3.23 → 0.3.25

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/dist/yapi-cli.js CHANGED
@@ -12,14 +12,40 @@ const path_1 = __importDefault(require("path"));
12
12
  const readline_1 = __importDefault(require("readline"));
13
13
  const markdown_1 = require("./docs/markdown");
14
14
  const install_1 = require("./skill/install");
15
+ const metadata_1 = require("./skill/metadata");
15
16
  const auth_1 = require("./services/yapi/auth");
16
17
  const authCache_1 = require("./services/yapi/authCache");
18
+ class HttpStatusError extends Error {
19
+ status;
20
+ statusText;
21
+ body;
22
+ endpoint;
23
+ constructor(endpoint, status, statusText, body) {
24
+ super(`request failed: ${status} ${statusText} ${body}`);
25
+ this.name = "HttpStatusError";
26
+ this.status = status;
27
+ this.statusText = statusText;
28
+ this.body = body;
29
+ this.endpoint = endpoint;
30
+ }
31
+ }
17
32
  function parseKeyValue(raw) {
18
33
  if (!raw || !raw.includes("="))
19
34
  throw new Error("expected key=value");
20
35
  const idx = raw.indexOf("=");
21
36
  return [raw.slice(0, idx), raw.slice(idx + 1)];
22
37
  }
38
+ function parseQueryArg(raw) {
39
+ const trimmed = String(raw || "").trim().replace(/^\?/, "");
40
+ if (!trimmed)
41
+ throw new Error("expected key=value");
42
+ if (!trimmed.includes("&"))
43
+ return [parseKeyValue(trimmed)];
44
+ const items = Array.from(new URLSearchParams(trimmed).entries()).filter(([key]) => Boolean(key));
45
+ if (items.length)
46
+ return items;
47
+ return [parseKeyValue(trimmed)];
48
+ }
23
49
  function parseHeader(raw) {
24
50
  if (!raw || !raw.includes(":"))
25
51
  throw new Error("expected Header:Value");
@@ -34,6 +60,58 @@ function parseJsonMaybe(text) {
34
60
  return null;
35
61
  }
36
62
  }
63
+ function formatBytes(bytes) {
64
+ if (!Number.isFinite(bytes) || bytes <= 0)
65
+ return "0 B";
66
+ const units = ["B", "KiB", "MiB", "GiB"];
67
+ let value = bytes;
68
+ let index = 0;
69
+ while (value >= 1024 && index < units.length - 1) {
70
+ value /= 1024;
71
+ index += 1;
72
+ }
73
+ return `${value >= 10 || index === 0 ? value.toFixed(0) : value.toFixed(2)} ${units[index]}`;
74
+ }
75
+ function parseByteSize(raw) {
76
+ const text = String(raw || "");
77
+ const bytesMatch = text.match(/(\d+)\s*bytes?/i);
78
+ if (bytesMatch)
79
+ return Number(bytesMatch[1]);
80
+ const unitMatch = text.match(/(\d+(?:\.\d+)?)\s*(kib|kb|mib|mb|gib|gb)\b/i);
81
+ if (!unitMatch)
82
+ return null;
83
+ const value = Number(unitMatch[1]);
84
+ if (!Number.isFinite(value))
85
+ return null;
86
+ const unit = unitMatch[2].toLowerCase();
87
+ const factors = {
88
+ kb: 1000,
89
+ kib: 1024,
90
+ mb: 1000 * 1000,
91
+ mib: 1024 * 1024,
92
+ gb: 1000 * 1000 * 1000,
93
+ gib: 1024 * 1024 * 1024,
94
+ };
95
+ return Math.round(value * (factors[unit] || 1));
96
+ }
97
+ function parsePayloadLimit(text) {
98
+ const match = String(text || "").match(/(?:limit|max(?:imum)?(?:\s+body)?(?:\s+size)?)[^0-9]{0,12}(\d+(?:\.\d+)?\s*(?:bytes?|kib|kb|mib|mb|gib|gb))/i);
99
+ if (match)
100
+ return parseByteSize(match[1]);
101
+ return parseByteSize(text);
102
+ }
103
+ function findGitRoot(startDir) {
104
+ let current = path_1.default.resolve(startDir);
105
+ while (true) {
106
+ const candidate = path_1.default.join(current, ".git");
107
+ if (fs_1.default.existsSync(candidate))
108
+ return current;
109
+ const parent = path_1.default.dirname(current);
110
+ if (parent === current)
111
+ return null;
112
+ current = parent;
113
+ }
114
+ }
37
115
  function getPayloadMessage(payload) {
38
116
  if (!payload || typeof payload !== "object")
39
117
  return "";
@@ -814,6 +892,7 @@ function usage() {
814
892
  " yapi whoami [options]",
815
893
  " yapi search [options]",
816
894
  " yapi install-skill [options]",
895
+ " yapi self-update",
817
896
  "Options:",
818
897
  " --config <path> config file path (default: ~/.yapi/config.toml)",
819
898
  " --base-url <url> YApi base URL",
@@ -858,6 +937,9 @@ function usage() {
858
937
  " -h, --help show help",
859
938
  ].join("\n");
860
939
  }
940
+ function selfUpdateUsage() {
941
+ return ["Usage:", " yapi self-update", "", "Install the latest @leeguoo/yapi-mcp globally with npm."].join("\n");
942
+ }
861
943
  function docsSyncUsage() {
862
944
  return [
863
945
  "Usage:",
@@ -1705,6 +1787,20 @@ function resolveDocsSyncHome(startDir, ensure) {
1705
1787
  ensureDocsSyncReadme(home);
1706
1788
  return home;
1707
1789
  }
1790
+ function globalYapiHomeDir() {
1791
+ return path_1.default.resolve(process.env.YAPI_HOME || path_1.default.join(os_1.default.homedir(), ".yapi"));
1792
+ }
1793
+ function normalizeComparablePath(targetPath) {
1794
+ try {
1795
+ return fs_1.default.realpathSync.native(targetPath);
1796
+ }
1797
+ catch {
1798
+ return path_1.default.resolve(targetPath);
1799
+ }
1800
+ }
1801
+ function isGlobalDocsSyncHome(homeDir) {
1802
+ return normalizeComparablePath(homeDir) === normalizeComparablePath(globalYapiHomeDir());
1803
+ }
1708
1804
  function docsSyncConfigPath(homeDir) {
1709
1805
  return path_1.default.join(homeDir, "docs-sync.json");
1710
1806
  }
@@ -1819,6 +1915,44 @@ function normalizeBindingDir(rootDir, bindingDir) {
1819
1915
  return resolved;
1820
1916
  return relative;
1821
1917
  }
1918
+ function getBindingBaseDir(homeDir, rootDir, cwd) {
1919
+ if (!isGlobalDocsSyncHome(homeDir)) {
1920
+ return { baseDir: rootDir, gitRoot: findGitRoot(cwd), usedGitRoot: false };
1921
+ }
1922
+ const gitRoot = findGitRoot(cwd);
1923
+ if (gitRoot) {
1924
+ return { baseDir: gitRoot, gitRoot, usedGitRoot: true };
1925
+ }
1926
+ return { baseDir: path_1.default.resolve(cwd), gitRoot: null, usedGitRoot: false };
1927
+ }
1928
+ function normalizeBindingDirForContext(homeDir, rootDir, cwd, bindingDir) {
1929
+ const context = getBindingBaseDir(homeDir, rootDir, cwd);
1930
+ const resolved = path_1.default.isAbsolute(bindingDir)
1931
+ ? bindingDir
1932
+ : path_1.default.resolve(context.baseDir, bindingDir);
1933
+ const relative = path_1.default.relative(rootDir, resolved);
1934
+ if (!relative || relative === ".")
1935
+ return ".";
1936
+ if (relative.startsWith("..") || path_1.default.isAbsolute(relative))
1937
+ return resolved;
1938
+ return relative;
1939
+ }
1940
+ function resolveBindingDirForContext(homeDir, rootDir, cwd, bindingDir) {
1941
+ if (!bindingDir)
1942
+ return rootDir;
1943
+ if (path_1.default.isAbsolute(bindingDir))
1944
+ return bindingDir;
1945
+ const direct = path_1.default.resolve(rootDir, bindingDir);
1946
+ if (!isGlobalDocsSyncHome(homeDir))
1947
+ return direct;
1948
+ if (fs_1.default.existsSync(direct))
1949
+ return direct;
1950
+ const { baseDir } = getBindingBaseDir(homeDir, rootDir, cwd);
1951
+ const contextual = path_1.default.resolve(baseDir, bindingDir);
1952
+ if (fs_1.default.existsSync(contextual))
1953
+ return contextual;
1954
+ return direct;
1955
+ }
1822
1956
  function suggestDocsSyncDir(startDir) {
1823
1957
  const candidates = ["docs", "doc", "documentation", "release-notes"];
1824
1958
  for (const candidate of candidates) {
@@ -2028,6 +2162,37 @@ async function checkForUpdates(options) {
2028
2162
  }
2029
2163
  writeUpdateCache(cache);
2030
2164
  }
2165
+ function warnIfInstalledSkillsOutdated(options) {
2166
+ if (options.skip)
2167
+ return;
2168
+ const currentVersion = readVersion();
2169
+ if (!currentVersion || currentVersion === "unknown")
2170
+ return;
2171
+ const outdated = (0, metadata_1.findOutdatedSkillInstalls)(currentVersion);
2172
+ if (!outdated.length)
2173
+ return;
2174
+ const summary = outdated
2175
+ .map((item) => `${item.label}@${item.installedVersion || "unknown"}`)
2176
+ .join(", ");
2177
+ console.warn(`skill update available: installed ${summary}, current ${currentVersion}. Run: yapi install-skill --force`);
2178
+ }
2179
+ function runSelfUpdate(rawArgs) {
2180
+ if (rawArgs.includes("-h") || rawArgs.includes("--help")) {
2181
+ console.log(selfUpdateUsage());
2182
+ return 0;
2183
+ }
2184
+ try {
2185
+ (0, child_process_1.execFileSync)(resolveLocalBin("npm"), ["install", "-g", "@leeguoo/yapi-mcp@latest"], {
2186
+ stdio: "inherit",
2187
+ });
2188
+ console.log("Updated yapi CLI to latest.");
2189
+ return 0;
2190
+ }
2191
+ catch (error) {
2192
+ console.error(error instanceof Error ? error.message : String(error));
2193
+ return 2;
2194
+ }
2195
+ }
2031
2196
  async function fetchProjectInfo(projectId, baseUrl, request) {
2032
2197
  if (!projectId)
2033
2198
  return null;
@@ -2096,6 +2261,59 @@ function buildAddPayload(template, title, apiPath, catId, projectId) {
2096
2261
  tag: template.tag || [],
2097
2262
  };
2098
2263
  }
2264
+ function buildUpdatePayload(docId, title, markdown, html) {
2265
+ const payload = { id: docId, markdown, desc: html };
2266
+ if (title) {
2267
+ payload.title = title;
2268
+ }
2269
+ return payload;
2270
+ }
2271
+ function pickLargestMermaid(metrics) {
2272
+ return metrics
2273
+ .filter((item) => item.renderer === "mermaid")
2274
+ .sort((a, b) => b.renderedBytes - a.renderedBytes)[0];
2275
+ }
2276
+ function buildDocsSyncPreviewLine(item) {
2277
+ const parts = [
2278
+ `file=${item.fileName}`,
2279
+ `action=${item.action}`,
2280
+ `markdown=${formatBytes(item.markdownBytes)}`,
2281
+ `html=${formatBytes(item.htmlBytes)}`,
2282
+ `payload=${formatBytes(item.payloadBytes)}`,
2283
+ `path=${item.apiPath}`,
2284
+ ];
2285
+ if (item.docId) {
2286
+ parts.push(`doc_id=${item.docId}`);
2287
+ }
2288
+ if (item.largestMermaid) {
2289
+ parts.push(`largest_mermaid=#${item.largestMermaid.index}`, `largest_mermaid_svg=${formatBytes(item.largestMermaid.renderedBytes)}`);
2290
+ }
2291
+ return `preview ${parts.join(" ")}`;
2292
+ }
2293
+ function buildDocsSyncPayloadTooLargeMessage(fileName, preview, error) {
2294
+ const lines = [
2295
+ `413 Payload Too Large while syncing ${fileName}`,
2296
+ `- request payload: ${formatBytes(preview.payloadBytes)}`,
2297
+ `- markdown size: ${formatBytes(preview.markdownBytes)}`,
2298
+ `- rendered html size: ${formatBytes(preview.htmlBytes)}`,
2299
+ ];
2300
+ const limitBytes = parsePayloadLimit(error.body || error.message);
2301
+ if (limitBytes) {
2302
+ lines.push(`- server limit: ${formatBytes(limitBytes)}`);
2303
+ }
2304
+ else {
2305
+ lines.push("- server limit: unknown (response did not expose an exact value)");
2306
+ }
2307
+ if (preview.largestMermaid) {
2308
+ lines.push(`- largest Mermaid block: #${preview.largestMermaid.index} -> ${formatBytes(preview.largestMermaid.renderedBytes)}`);
2309
+ }
2310
+ else {
2311
+ lines.push("- largest Mermaid block: none");
2312
+ }
2313
+ lines.push("- suggestion: run `yapi docs-sync --dry-run ...` to preview all files before upload");
2314
+ lines.push("- suggestion: split oversized Mermaid diagrams or move them into separate docs");
2315
+ return lines.join("\n");
2316
+ }
2099
2317
  async function addInterface(title, apiPath, mapping, request) {
2100
2318
  const projectId = Number(mapping.project_id || 0);
2101
2319
  const catId = Number(mapping.catid || 0);
@@ -2122,10 +2340,7 @@ async function addInterface(title, apiPath, mapping, request) {
2122
2340
  return Number(newId);
2123
2341
  }
2124
2342
  async function updateInterface(docId, title, markdown, html, request) {
2125
- const payload = { id: docId, markdown, desc: html };
2126
- if (title) {
2127
- payload.title = title;
2128
- }
2343
+ const payload = buildUpdatePayload(docId, title, markdown, html);
2129
2344
  const resp = await request("/api/interface/up", "POST", {}, payload);
2130
2345
  if (resp?.errcode !== 0) {
2131
2346
  throw new Error(`interface up failed: ${resp?.errmsg || "unknown error"}`);
@@ -2147,14 +2362,19 @@ async function syncDocsDir(dirPath, mapping, options, request) {
2147
2362
  mapping.catid = Number(envCatId);
2148
2363
  if (!mapping.template_id && envTemplateId)
2149
2364
  mapping.template_id = Number(envTemplateId);
2150
- if (!mapping.project_id || !mapping.catid) {
2365
+ const hasTarget = Boolean(mapping.project_id && mapping.catid);
2366
+ if (!hasTarget && !options.dryRun) {
2151
2367
  throw new Error("缺少 project_id/catid。请先绑定或配置:yapi docs-sync bind add --name <binding> --dir <path> --project-id <id> --catid <id>,或在目录下添加 .yapi.json,或设置环境变量 YAPI_PROJECT_ID/YAPI_CATID。");
2152
2368
  }
2153
- const { byPath, byTitle, byId } = await listExistingInterfaces(Number(mapping.catid), request);
2369
+ const { byPath, byTitle, byId } = hasTarget
2370
+ ? await listExistingInterfaces(Number(mapping.catid), request)
2371
+ : { byPath: {}, byTitle: {}, byId: {} };
2154
2372
  let updated = 0;
2155
2373
  let created = 0;
2156
2374
  let skipped = 0;
2375
+ let previewOnly = 0;
2157
2376
  const fileInfos = {};
2377
+ const previews = [];
2158
2378
  const files = resolveSourceFiles(dirPath, mapping);
2159
2379
  for (const mdPath of files) {
2160
2380
  const stem = path_1.default.parse(mdPath).name;
@@ -2168,43 +2388,34 @@ async function syncDocsDir(dirPath, mapping, options, request) {
2168
2388
  if (docId)
2169
2389
  mapping.files[relName] = docId;
2170
2390
  }
2171
- if (!docId) {
2391
+ let action = docId ? "update" : hasTarget ? "create" : "preview-only";
2392
+ if (!docId && hasTarget) {
2172
2393
  created += 1;
2173
2394
  if (!options.dryRun) {
2174
2395
  docId = await addInterface(desiredTitle, apiPath, mapping, request);
2175
2396
  mapping.files[relName] = docId;
2176
2397
  }
2177
2398
  }
2399
+ if (!docId && !hasTarget) {
2400
+ previewOnly += 1;
2401
+ }
2178
2402
  if (docId) {
2179
2403
  const resolvedPath = byId[String(docId)]?.path || apiPath;
2180
2404
  fileInfos[relName] = { docId: Number(docId), apiPath: resolvedPath };
2181
2405
  }
2182
- const contentHash = buildDocsSyncHash(markdown, options);
2183
- const previousHash = mapping.file_hashes[relName];
2184
- const currentTitle = docId ? byId[String(docId)]?.title : "";
2185
- const titleToUpdate = !docId
2186
- ? undefined
2187
- : !currentTitle || currentTitle !== desiredTitle
2188
- ? desiredTitle
2189
- : undefined;
2190
- const shouldSyncTitle = Boolean(titleToUpdate);
2191
- if (!options.force &&
2192
- docId &&
2193
- previousHash &&
2194
- previousHash === contentHash &&
2195
- !shouldSyncTitle) {
2196
- skipped += 1;
2197
- continue;
2198
- }
2199
2406
  const logPrefix = `[docs-sync:${relName}]`;
2200
2407
  let mermaidFailed = false;
2201
2408
  let diagramFailed = false;
2409
+ const diagramMetrics = [];
2202
2410
  const html = (0, markdown_1.renderMarkdownToHtml)(markdown, {
2203
2411
  noMermaid: options.noMermaid,
2204
2412
  logMermaid: true,
2205
2413
  mermaidLook: options.mermaidLook,
2206
2414
  mermaidHandDrawnSeed: options.mermaidHandDrawnSeed,
2207
2415
  logger: (message) => console.log(`${logPrefix} ${message}`),
2416
+ onDiagramRendered: (metric) => {
2417
+ diagramMetrics.push(metric);
2418
+ },
2208
2419
  onMermaidError: () => {
2209
2420
  mermaidFailed = true;
2210
2421
  },
@@ -2212,15 +2423,56 @@ async function syncDocsDir(dirPath, mapping, options, request) {
2212
2423
  diagramFailed = true;
2213
2424
  },
2214
2425
  });
2215
- if (!options.dryRun && docId) {
2216
- await updateInterface(docId, titleToUpdate, markdown, html, request);
2426
+ const contentHash = buildDocsSyncHash(markdown, options);
2427
+ const previousHash = mapping.file_hashes[relName];
2428
+ const currentTitle = docId ? byId[String(docId)]?.title : "";
2429
+ const titleToUpdate = !docId
2430
+ ? undefined
2431
+ : !currentTitle || currentTitle !== desiredTitle
2432
+ ? desiredTitle
2433
+ : undefined;
2434
+ const shouldSyncTitle = Boolean(titleToUpdate);
2435
+ if (!options.force &&
2436
+ docId &&
2437
+ previousHash &&
2438
+ previousHash === contentHash &&
2439
+ !shouldSyncTitle) {
2440
+ action = "skip";
2441
+ skipped += 1;
2442
+ }
2443
+ const payloadObject = docId && action !== "create"
2444
+ ? buildUpdatePayload(docId, titleToUpdate, markdown, html)
2445
+ : buildAddPayload({}, desiredTitle, apiPath, Number(mapping.catid || 0), Number(mapping.project_id || 0));
2446
+ const preview = {
2447
+ fileName: relName,
2448
+ action,
2449
+ markdownBytes: Buffer.byteLength(markdown, "utf8"),
2450
+ htmlBytes: Buffer.byteLength(html, "utf8"),
2451
+ payloadBytes: Buffer.byteLength(JSON.stringify(payloadObject), "utf8"),
2452
+ apiPath: docId ? fileInfos[relName]?.apiPath || apiPath : apiPath,
2453
+ docId: docId ? Number(docId) : undefined,
2454
+ largestMermaid: pickLargestMermaid(diagramMetrics),
2455
+ };
2456
+ previews.push(preview);
2457
+ if (!options.dryRun && docId && action !== "skip") {
2458
+ try {
2459
+ await updateInterface(docId, titleToUpdate, markdown, html, request);
2460
+ }
2461
+ catch (error) {
2462
+ if (error instanceof HttpStatusError && error.status === 413) {
2463
+ throw new Error(buildDocsSyncPayloadTooLargeMessage(relName, preview, error));
2464
+ }
2465
+ throw error;
2466
+ }
2217
2467
  }
2218
2468
  if (docId && !mermaidFailed && !diagramFailed) {
2219
2469
  mapping.file_hashes[relName] = contentHash;
2220
2470
  }
2221
- updated += 1;
2471
+ if (action !== "skip") {
2472
+ updated += 1;
2473
+ }
2222
2474
  }
2223
- return { updated, created, skipped, files: fileInfos };
2475
+ return { updated, created, skipped, previewOnly, files: fileInfos, previews };
2224
2476
  }
2225
2477
  function buildEnvUrls(projectInfo, apiPath) {
2226
2478
  const urls = {};
@@ -2395,7 +2647,7 @@ async function runDocsSyncBindings(rawArgs) {
2395
2647
  file_hashes: existing.file_hashes ? { ...existing.file_hashes } : {},
2396
2648
  };
2397
2649
  if (options.dir) {
2398
- next.dir = normalizeBindingDir(rootDir, options.dir);
2650
+ next.dir = normalizeBindingDirForContext(homeDir, rootDir, process.cwd(), options.dir);
2399
2651
  }
2400
2652
  if (options.projectId !== undefined && Number.isFinite(options.projectId)) {
2401
2653
  next.project_id = Number(options.projectId);
@@ -2418,7 +2670,17 @@ async function runDocsSyncBindings(rawArgs) {
2418
2670
  }
2419
2671
  config.bindings[options.name] = next;
2420
2672
  saveDocsSyncConfig(homeDir, config);
2673
+ const resolvedDir = resolveBindingDirForContext(homeDir, rootDir, process.cwd(), next.dir);
2421
2674
  console.log(`${action === "add" ? "binding added" : "binding updated"}: ${options.name}`);
2675
+ console.log(`stored_dir=${next.dir}`);
2676
+ console.log(`resolved_dir=${resolvedDir}`);
2677
+ const gitRoot = findGitRoot(process.cwd());
2678
+ if (gitRoot) {
2679
+ console.log(`git_root=${gitRoot}`);
2680
+ }
2681
+ else if (isGlobalDocsSyncHome(homeDir)) {
2682
+ console.warn("warning: no git root detected, relative --dir was resolved from current working directory");
2683
+ }
2422
2684
  return 0;
2423
2685
  }
2424
2686
  async function runDocsSync(rawArgs) {
@@ -2626,7 +2888,7 @@ async function runDocsSync(rawArgs) {
2626
2888
  result = await sendOnce();
2627
2889
  }
2628
2890
  if (!result.response.ok) {
2629
- throw new Error(`request failed: ${result.response.status} ${result.response.statusText} ${result.text}`);
2891
+ throw new HttpStatusError(endpoint, result.response.status, result.response.statusText, result.text);
2630
2892
  }
2631
2893
  if (!result.json) {
2632
2894
  throw new Error(`invalid JSON response from ${endpoint}`);
@@ -2642,7 +2904,7 @@ async function runDocsSync(rawArgs) {
2642
2904
  if (!binding) {
2643
2905
  throw new Error(`binding not found: ${name}`);
2644
2906
  }
2645
- const dirPath = resolveBindingDir(rootDir, binding.dir);
2907
+ const dirPath = resolveBindingDirForContext(docsSyncHome, rootDir, process.cwd(), binding.dir);
2646
2908
  const existing = dirToBindings.get(dirPath) || [];
2647
2909
  existing.push(name);
2648
2910
  dirToBindings.set(dirPath, existing);
@@ -2667,12 +2929,16 @@ async function runDocsSync(rawArgs) {
2667
2929
  if (!binding) {
2668
2930
  throw new Error(`binding not found: ${name}`);
2669
2931
  }
2670
- const dirPath = resolveBindingDir(rootDir, binding.dir);
2932
+ const dirPath = resolveBindingDirForContext(docsSyncHome, rootDir, process.cwd(), binding.dir);
2671
2933
  if (!fs_1.default.existsSync(dirPath) || !fs_1.default.statSync(dirPath).isDirectory()) {
2672
2934
  throw new Error(`dir not found for binding ${name}: ${dirPath}`);
2673
2935
  }
2674
2936
  const result = await syncDocsDir(dirPath, binding, options, request);
2675
- console.log(`synced=${result.updated} created=${result.created} skipped=${result.skipped} binding=${name} dir=${dirPath}`);
2937
+ if (options.dryRun) {
2938
+ console.log(`dry-run preview binding=${name}`);
2939
+ result.previews.forEach((item) => console.log(buildDocsSyncPreviewLine(item)));
2940
+ }
2941
+ console.log(`synced=${result.updated} created=${result.created} skipped=${result.skipped} preview_only=${result.previewOnly} binding=${name} dir=${dirPath}`);
2676
2942
  bindingResults[name] = { binding, files: result.files };
2677
2943
  }
2678
2944
  if (!options.dryRun) {
@@ -2688,10 +2954,14 @@ async function runDocsSync(rawArgs) {
2688
2954
  }
2689
2955
  const { mapping, mappingPath } = loadMapping(dirPath);
2690
2956
  const result = await syncDocsDir(dirPath, mapping, options, request);
2957
+ if (options.dryRun) {
2958
+ console.log(`dry-run preview dir=${dirPath}`);
2959
+ result.previews.forEach((item) => console.log(buildDocsSyncPreviewLine(item)));
2960
+ }
2691
2961
  if (!options.dryRun) {
2692
2962
  saveMapping(mapping, mappingPath);
2693
2963
  }
2694
- console.log(`synced=${result.updated} created=${result.created} skipped=${result.skipped} dir=${dirPath}`);
2964
+ console.log(`synced=${result.updated} created=${result.created} skipped=${result.skipped} preview_only=${result.previewOnly} dir=${dirPath}`);
2695
2965
  }
2696
2966
  }
2697
2967
  return 0;
@@ -2704,12 +2974,21 @@ async function runDocsSync(rawArgs) {
2704
2974
  async function main() {
2705
2975
  const rawArgs = process.argv.slice(2);
2706
2976
  const parsedForUpdate = parseArgs(rawArgs);
2977
+ const subcommand = rawArgs[0] || "";
2707
2978
  const skipUpdateCheck = parsedForUpdate.version ||
2708
2979
  parsedForUpdate.help ||
2709
2980
  parsedForUpdate.noUpdate ||
2710
2981
  rawArgs.includes("-h") ||
2711
2982
  rawArgs.includes("--help");
2712
2983
  await checkForUpdates({ noUpdate: parsedForUpdate.noUpdate, skip: skipUpdateCheck });
2984
+ const skipSkillWarning = skipUpdateCheck ||
2985
+ subcommand === "install-skill" ||
2986
+ subcommand === "self-update" ||
2987
+ process.env.YAPI_NO_SKILL_UPDATE_CHECK === "1";
2988
+ warnIfInstalledSkillsOutdated({ skip: skipSkillWarning });
2989
+ if (subcommand === "self-update") {
2990
+ return runSelfUpdate(rawArgs.slice(1));
2991
+ }
2713
2992
  if (rawArgs[0] === "install-skill") {
2714
2993
  await (0, install_1.runInstallSkill)(rawArgs.slice(1));
2715
2994
  return 0;
@@ -2862,7 +3141,7 @@ async function main() {
2862
3141
  }
2863
3142
  const queryItems = [];
2864
3143
  for (const query of options.query || []) {
2865
- queryItems.push(parseKeyValue(query));
3144
+ queryItems.push(...parseQueryArg(query));
2866
3145
  }
2867
3146
  const url = buildUrl(baseUrl, endpoint, queryItems, authMode === "token" ? token : "", options.tokenParam || "token");
2868
3147
  let dataRaw = null;