From 6ccda6cb6107c028c319fce2cadec69c09c214d6 Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Tue, 8 Sep 2026 18:05:29 +0000 Subject: [PATCH 1/3] feat(cli): add docs Co-authored-by: Cursor --- packages/core/bin/docs.js | 42 ++++++++++++++++++++++++ packages/core/bin/help.js | 17 ++++++---- packages/core/bin/run.js | 20 ++++++++++++ packages/core/test/cli.mjs | 27 +++++++++++++++ packages/core/test/docs.mjs | 65 +++++++++++++++++++++++++++++++++++++ 5 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 packages/core/bin/docs.js create mode 100644 packages/core/test/docs.mjs diff --git a/packages/core/bin/docs.js b/packages/core/bin/docs.js new file mode 100644 index 0000000..6548f1c --- /dev/null +++ b/packages/core/bin/docs.js @@ -0,0 +1,42 @@ +'use strict' + +const SITE = 'https://microlink.io' + +const HREF = { + metadata: '/docs/sdk/methods/metadata', + screenshot: '/docs/sdk/methods/screenshot', + pdf: '/docs/sdk/methods/pdf', + markdown: '/docs/sdk/methods/markdown', + html: '/docs/sdk/methods/html', + text: '/docs/sdk/methods/text', + function: '/docs/sdk/methods/function', + search: '/docs/sdk/methods/search', + extract: '/docs/sdk/methods/extract', + logo: '/docs/sdk/methods/logo', + emails: '/docs/sdk/methods/collections/emails', + links: '/docs/sdk/methods/collections/links', + images: '/docs/sdk/methods/collections/images', + videos: '/docs/sdk/methods/collections/videos', + audios: '/docs/sdk/methods/collections/audios', + video: '/docs/sdk/methods/media/video', + audio: '/docs/sdk/methods/media/audio', + embed: '/docs/sdk/methods/embed', + technologies: '/docs/sdk/methods/insights/technologies', + lighthouse: '/docs/sdk/methods/insights/lighthouse' +} + +const resolve = name => { + const href = HREF[String(name).toLowerCase().replace(/^\./, '')] + return href ? { href } : { error: 'unknown' } +} + +const toMarkdownUrl = href => `${SITE}${href.replace(/\/+$/, '')}.md` + +const load = async (href, fetchFn = fetch) => { + const url = toMarkdownUrl(href) + const res = await fetchFn(url) + if (!res.ok) throw new Error(`Failed to fetch ${url} (${res.status})`) + return res.text() +} + +module.exports = { resolve, load, toMarkdownUrl, HREF } diff --git a/packages/core/bin/help.js b/packages/core/bin/help.js index 09b468d..57ddb2e 100644 --- a/packages/core/bin/help.js +++ b/packages/core/bin/help.js @@ -301,6 +301,7 @@ const commandList = Object.entries(COMMANDS) const global = `Usage ${cmd(' [options]')} ${cmd(' [options]')} +${cmd(' docs')} ${cmd('help')} ${cmd('login')} ${cmd('logout')} @@ -316,6 +317,7 @@ ${rows(CLI)} Examples ${cmd('login', 'save an API key from your account')} +${cmd('markdown docs', 'print the markdown docs page')} ${cmd('https://example.com', 'unified metadata (default)')} ${cmd( 'https://example.com --trace', @@ -348,21 +350,24 @@ ${cmd( ` const render = (name, product) => { - const usage = [] - .concat(product.usage) - .map(line => cmd(line)) - .join('\n') + const usageLines = [].concat(product.usage) + const examples = [...(product.examples ?? [])] + if (PRODUCTS[name]) { + usageLines.push(`${name} docs`) + examples.push([`${name} docs`, 'print the docs page']) + } + const usage = usageLines.map(line => cmd(line)).join('\n') const cli = product.cli ?? CLI const options = [...product.flags, ...cli] const parts = ['Usage', usage, '', gray(product.desc)] if (options.length > 0) parts.push('', 'Options', rows(options)) if (product.browser) parts.push('', 'Browser', rows(BROWSER)) if (product.note) parts.push('', gray(product.note)) - if (product.examples) { + if (examples.length > 0) { parts.push( '', 'Examples', - ...product.examples.map(([rest, comment]) => cmd(rest, comment)) + ...examples.map(([rest, comment]) => cmd(rest, comment)) ) } return parts.join('\n') + '\n' diff --git a/packages/core/bin/run.js b/packages/core/bin/run.js index 88c67c0..134b149 100644 --- a/packages/core/bin/run.js +++ b/packages/core/bin/run.js @@ -6,6 +6,7 @@ const spinner = require('./spinner') const parseArgv = require('./argv') const helpText = require('./help') const { asUrl } = require('./url') +const docs = require('./docs') const create = require('../src') const run = async (argvInput, host) => { @@ -86,6 +87,25 @@ const run = async (argvInput, host) => { } if (help || !target) return showHelp(command) + + if (target === 'docs') { + const resolved = docs.resolve(command) + if (resolved.error) { + writeLine(stderr, `No docs page for \`${command}\`.`) + return finish(1) + } + try { + writeLine( + stdout, + (await docs.load(resolved.href, host.fetch ?? fetch)).trimEnd() + ) + return finish(0) + } catch (error) { + writeLine(stderr, error.message) + return finish(1) + } + } + if (command !== 'search') target = asUrl(target) ?? target if (isTrace && (command === 'search' || command === 'function')) { diff --git a/packages/core/test/cli.mjs b/packages/core/test/cli.mjs index 43c4aff..4966c90 100644 --- a/packages/core/test/cli.mjs +++ b/packages/core/test/cli.mjs @@ -23,6 +23,7 @@ test('prints help with no arguments', async t => { t.true(stdout.includes('--endpoint')) t.true(stdout.includes('login')) t.true(stdout.includes('logout')) + t.true(stdout.includes(' docs')) }) test('help command matches --help', async t => { @@ -50,6 +51,18 @@ test('prints command help for logout', async t => { t.true(stdout.includes('Remove the saved API key')) }) +test('product help includes the docs usage', async t => { + const { stdout } = await $('node', [bin, 'markdown', '--help']) + t.true(stdout.includes('markdown docs')) + t.true(stdout.includes('print the docs page')) +}) + +test('markdown docs prints the docs page', async t => { + const { stdout } = await $('node', [bin, 'markdown', 'docs']) + t.true(stdout.includes('# markdown')) + t.true(stdout.includes('The page as clean Markdown')) +}) + test('prints command help for a product with no url', async t => { const { stdout } = await $('node', [bin, 'metadata']) t.true(stdout.includes('metadata ')) @@ -509,6 +522,20 @@ test('run reports unknown commands through the host', async t => { t.true(host.stderrText().includes('Unknown command')) }) +test('run docs writes the fetched markdown through the host', async t => { + const host = memoryHost({ + fetch: url => { + t.is(url, 'https://microlink.io/docs/sdk/methods/markdown.md') + return Promise.resolve({ + ok: true, + text: () => Promise.resolve('# markdown\n') + }) + } + }) + t.is(await run(['markdown', 'docs'], host), 0) + t.is(host.stdoutText().trim(), '# markdown') +}) + test('run reports missing --file through the host', async t => { const host = memoryHost() t.is(await run(['function', 'https://example.com'], host), 1) diff --git a/packages/core/test/docs.mjs b/packages/core/test/docs.mjs new file mode 100644 index 0000000..7897642 --- /dev/null +++ b/packages/core/test/docs.mjs @@ -0,0 +1,65 @@ +import { createRequire } from 'module' +import test from 'ava' + +const require = createRequire(import.meta.url) +const { resolve, load, toMarkdownUrl, HREF } = require('../bin/docs') + +const PRODUCTS = [ + 'metadata', + 'logo', + 'markdown', + 'html', + 'text', + 'video', + 'audio', + 'emails', + 'links', + 'images', + 'videos', + 'audios', + 'extract', + 'screenshot', + 'pdf', + 'embed', + 'technologies', + 'lighthouse', + 'search', + 'function' +] + +test('every product maps to an SDK docs page', t => { + t.deepEqual(Object.keys(HREF).sort(), PRODUCTS.sort()) + for (const name of PRODUCTS) { + t.true(HREF[name].startsWith('/docs/sdk/'), name) + t.deepEqual(resolve(name), { href: HREF[name] }) + } +}) + +test('accepts a leading dot the sidebar uses for methods', t => { + t.deepEqual(resolve('.markdown'), { href: '/docs/sdk/methods/markdown' }) +}) + +test('unknown product', t => { + t.deepEqual(resolve('nope'), { error: 'unknown' }) +}) + +test('load fetches the markdown file', async t => { + const href = '/docs/sdk/methods/markdown' + const text = await load(href, url => { + t.is(url, toMarkdownUrl(href)) + return Promise.resolve({ + ok: true, + text: () => Promise.resolve('# markdown\n') + }) + }) + t.is(text, '# markdown\n') +}) + +test('load throws when the page is missing', async t => { + const error = await t.throwsAsync(() => + load('/docs/sdk/methods/markdown', () => + Promise.resolve({ ok: false, status: 404 }) + ) + ) + t.true(error.message.includes('404')) +}) From 601075381ea57c9a4938ebc0bc5ed91b367f607b Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Tue, 8 Sep 2026 18:11:07 +0000 Subject: [PATCH 2/3] refactor(cli): derive docs url from product name Co-authored-by: Cursor --- packages/core/bin/docs.js | 42 +++++-------------------------- packages/core/bin/run.js | 7 +----- packages/core/test/docs.mjs | 50 +++++-------------------------------- 3 files changed, 13 insertions(+), 86 deletions(-) diff --git a/packages/core/bin/docs.js b/packages/core/bin/docs.js index 6548f1c..8187a4f 100644 --- a/packages/core/bin/docs.js +++ b/packages/core/bin/docs.js @@ -1,42 +1,12 @@ 'use strict' -const SITE = 'https://microlink.io' +const url = product => `https://microlink.io/docs/sdk/methods/${product}.md` -const HREF = { - metadata: '/docs/sdk/methods/metadata', - screenshot: '/docs/sdk/methods/screenshot', - pdf: '/docs/sdk/methods/pdf', - markdown: '/docs/sdk/methods/markdown', - html: '/docs/sdk/methods/html', - text: '/docs/sdk/methods/text', - function: '/docs/sdk/methods/function', - search: '/docs/sdk/methods/search', - extract: '/docs/sdk/methods/extract', - logo: '/docs/sdk/methods/logo', - emails: '/docs/sdk/methods/collections/emails', - links: '/docs/sdk/methods/collections/links', - images: '/docs/sdk/methods/collections/images', - videos: '/docs/sdk/methods/collections/videos', - audios: '/docs/sdk/methods/collections/audios', - video: '/docs/sdk/methods/media/video', - audio: '/docs/sdk/methods/media/audio', - embed: '/docs/sdk/methods/embed', - technologies: '/docs/sdk/methods/insights/technologies', - lighthouse: '/docs/sdk/methods/insights/lighthouse' -} - -const resolve = name => { - const href = HREF[String(name).toLowerCase().replace(/^\./, '')] - return href ? { href } : { error: 'unknown' } -} - -const toMarkdownUrl = href => `${SITE}${href.replace(/\/+$/, '')}.md` - -const load = async (href, fetchFn = fetch) => { - const url = toMarkdownUrl(href) - const res = await fetchFn(url) - if (!res.ok) throw new Error(`Failed to fetch ${url} (${res.status})`) +const load = async (product, fetchFn = fetch) => { + const href = url(product) + const res = await fetchFn(href) + if (!res.ok) throw new Error(`Failed to fetch ${href} (${res.status})`) return res.text() } -module.exports = { resolve, load, toMarkdownUrl, HREF } +module.exports = { load, url } diff --git a/packages/core/bin/run.js b/packages/core/bin/run.js index 134b149..7178f3d 100644 --- a/packages/core/bin/run.js +++ b/packages/core/bin/run.js @@ -89,15 +89,10 @@ const run = async (argvInput, host) => { if (help || !target) return showHelp(command) if (target === 'docs') { - const resolved = docs.resolve(command) - if (resolved.error) { - writeLine(stderr, `No docs page for \`${command}\`.`) - return finish(1) - } try { writeLine( stdout, - (await docs.load(resolved.href, host.fetch ?? fetch)).trimEnd() + (await docs.load(command, host.fetch ?? fetch)).trimEnd() ) return finish(0) } catch (error) { diff --git a/packages/core/test/docs.mjs b/packages/core/test/docs.mjs index 7897642..f8da4db 100644 --- a/packages/core/test/docs.mjs +++ b/packages/core/test/docs.mjs @@ -2,51 +2,15 @@ import { createRequire } from 'module' import test from 'ava' const require = createRequire(import.meta.url) -const { resolve, load, toMarkdownUrl, HREF } = require('../bin/docs') +const { load, url } = require('../bin/docs') -const PRODUCTS = [ - 'metadata', - 'logo', - 'markdown', - 'html', - 'text', - 'video', - 'audio', - 'emails', - 'links', - 'images', - 'videos', - 'audios', - 'extract', - 'screenshot', - 'pdf', - 'embed', - 'technologies', - 'lighthouse', - 'search', - 'function' -] - -test('every product maps to an SDK docs page', t => { - t.deepEqual(Object.keys(HREF).sort(), PRODUCTS.sort()) - for (const name of PRODUCTS) { - t.true(HREF[name].startsWith('/docs/sdk/'), name) - t.deepEqual(resolve(name), { href: HREF[name] }) - } -}) - -test('accepts a leading dot the sidebar uses for methods', t => { - t.deepEqual(resolve('.markdown'), { href: '/docs/sdk/methods/markdown' }) -}) - -test('unknown product', t => { - t.deepEqual(resolve('nope'), { error: 'unknown' }) +test('points at the SDK method markdown file', t => { + t.is(url('markdown'), 'https://microlink.io/docs/sdk/methods/markdown.md') }) test('load fetches the markdown file', async t => { - const href = '/docs/sdk/methods/markdown' - const text = await load(href, url => { - t.is(url, toMarkdownUrl(href)) + const text = await load('markdown', href => { + t.is(href, url('markdown')) return Promise.resolve({ ok: true, text: () => Promise.resolve('# markdown\n') @@ -57,9 +21,7 @@ test('load fetches the markdown file', async t => { test('load throws when the page is missing', async t => { const error = await t.throwsAsync(() => - load('/docs/sdk/methods/markdown', () => - Promise.resolve({ ok: false, status: 404 }) - ) + load('markdown', () => Promise.resolve({ ok: false, status: 404 })) ) t.true(error.message.includes('404')) }) From 16f02345d5e96ffe7a6d9e87d77151186544e4da Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Tue, 8 Sep 2026 18:16:45 +0000 Subject: [PATCH 3/3] fix(cli): abort hung docs fetches Co-authored-by: Cursor --- packages/core/bin/docs.js | 2 +- packages/core/test/cli.mjs | 6 ------ packages/core/test/docs.mjs | 3 ++- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/core/bin/docs.js b/packages/core/bin/docs.js index 8187a4f..783d4c7 100644 --- a/packages/core/bin/docs.js +++ b/packages/core/bin/docs.js @@ -4,7 +4,7 @@ const url = product => `https://microlink.io/docs/sdk/methods/${product}.md` const load = async (product, fetchFn = fetch) => { const href = url(product) - const res = await fetchFn(href) + const res = await fetchFn(href, { signal: AbortSignal.timeout(10_000) }) if (!res.ok) throw new Error(`Failed to fetch ${href} (${res.status})`) return res.text() } diff --git a/packages/core/test/cli.mjs b/packages/core/test/cli.mjs index 4966c90..f20fd99 100644 --- a/packages/core/test/cli.mjs +++ b/packages/core/test/cli.mjs @@ -57,12 +57,6 @@ test('product help includes the docs usage', async t => { t.true(stdout.includes('print the docs page')) }) -test('markdown docs prints the docs page', async t => { - const { stdout } = await $('node', [bin, 'markdown', 'docs']) - t.true(stdout.includes('# markdown')) - t.true(stdout.includes('The page as clean Markdown')) -}) - test('prints command help for a product with no url', async t => { const { stdout } = await $('node', [bin, 'metadata']) t.true(stdout.includes('metadata ')) diff --git a/packages/core/test/docs.mjs b/packages/core/test/docs.mjs index f8da4db..fb5726f 100644 --- a/packages/core/test/docs.mjs +++ b/packages/core/test/docs.mjs @@ -9,8 +9,9 @@ test('points at the SDK method markdown file', t => { }) test('load fetches the markdown file', async t => { - const text = await load('markdown', href => { + const text = await load('markdown', (href, opts) => { t.is(href, url('markdown')) + t.true(opts.signal instanceof AbortSignal) return Promise.resolve({ ok: true, text: () => Promise.resolve('# markdown\n')