diff --git a/README.md b/README.md index 43475eb..b941659 100644 --- a/README.md +++ b/README.md @@ -124,14 +124,21 @@ Works on any mostly-static site with no per-site setup: news sites, blogs, docum | Wikipedia | `oc wiki` (via `action=render`) | `article `, `search <query>`, `lang <code> <title>` | | AWS docs | `oc aws` (search via DuckDuckGo) | `guide <service> <page>`, `page <service> <guide> <page>`, `cli <command>`, `search <query>` | | Google Cloud docs | `oc gcp` (via docs.cloud.google.com, search via DuckDuckGo) | `docs <product>`, `page <product> <page>`, `gcloud <command>`, `search <query>` | -| Microsoft Learn | `oc learn` (search via its RSS API) | `azure <page>`, `doc <path>`, `cli <command>`, `search <query>` | +| Microsoft Learn | `oc learn` (search via its RSS API, covers .NET) | `azure <page>`, `doc <path>`, `dotnet <api>`, `cli <command>`, `search <query>` | | Python docs | `oc py` (search via the docs' own index) | `library <module>`, `doc <path>`, `search <query>` | | MDN | `oc mdn` (search via the site's own API) | `js <page>`, `css <page>`, `doc <path>`, `search <query>` | | Node.js docs | `oc node` (search via the docs' own reference) | `api <module>`, `search <query>` | +| Ruby docs | `oc ruby` (search via the docs' own index) | `class <class>`, `search <query>` | +| Go packages | `oc go` (pkg.go.dev, server-rendered search) | `pkg <path>`, `search <query>` | +| PHP manual | `oc php` (an exact `fn` name lands on its page, search via DuckDuckGo) | `fn <name>`, `doc <path>`, `search <query>` | +| Rust docs | `oc rust` (search via DuckDuckGo) | `std <path>`, `doc <path>`, `search <query>` | +| Java docs | `oc java` (Javadoc for the current JDK, search via DuckDuckGo) | `api <path>`, `search <query>` | +| C and C++ | `oc cpp` (cppreference.com, search via DuckDuckGo) | `cpp <path>`, `c <path>`, `search <query>` | +| TypeScript | `oc ts` (search via DuckDuckGo) | `handbook <page>`, `search <query>` | A shortcut only ever resolves to a URL and then takes the same path `oc open` does, so it changes nothing about what a page costs or how it reads. The last argument takes every word after it, so `oc ddg search claude code cli` and `oc aws search s3 lifecycle rules` need no quoting, and a path argument keeps its slashes, so `oc learn doc azure/aks/what-is-aks` reaches that page. -A few of these (X, Stack Overflow, YouTube, Microsoft Learn search) read pages that look login-gated or JS-only from the outside, by finding the server-rendered HTML, feed, inline data, or public API the page already ships without a login. Stack Overflow search goes through the Stack Exchange API, and each result prints its `question_id`: read one with the `question <id>` feed rather than following its link, since the question page itself answers a bot challenge instead of the question. AWS and Google Cloud render docs search client-side, so their `search` goes through DuckDuckGo with a baked-in `site:` filter instead. Python's docs are built with Sphinx, which publishes the site's full-text search index as one static file, so `oc py search` fetches that index (cached on disk for a day), ranks it locally, and prints a numbered result list; a query that names a symbol exactly, like `json.dumps`, links straight to its anchor. The same backend will work for any Sphinx site, including most Read the Docs projects. MDN also renders its search client-side, but the page gets its results from a public JSON endpoint, so `oc mdn search` asks that endpoint directly and prints the site's own ranking; that `api` shape in a site definition works for any site whose search answers as JSON. Node.js ships no search endpoint at all, but publishes its whole API reference as one static JSON file, so `oc node search` ranks that file locally the same way the Sphinx backend does, under the same day cache, and every module, class, method, property, and event heading links to its own anchor. Not supported yet: pages that only render with JavaScript, sites behind logins, and sites with hard bot challenges that expose no feed. +A few of these (X, Stack Overflow, YouTube, Microsoft Learn search) read pages that look login-gated or JS-only from the outside, by finding the server-rendered HTML, feed, inline data, or public API the page already ships without a login. Stack Overflow search goes through the Stack Exchange API, and each result prints its `question_id`: read one with the `question <id>` feed rather than following its link, since the question page itself answers a bot challenge instead of the question. AWS, Google Cloud, Rust, Java, TypeScript, PHP, and cppreference render docs search client-side, or as a page too bare for oc to read, so their `search` goes through DuckDuckGo with a baked-in `site:` filter instead; Go needs no such fallback, because pkg.go.dev renders its search results on the server and `oc go search` simply opens them. Python's docs are built with Sphinx, which publishes the site's full-text search index as one static file, so `oc py search` fetches that index (cached on disk for a day), ranks it locally, and prints a numbered result list; a query that names a symbol exactly, like `json.dumps`, links straight to its anchor. The same backend will work for any Sphinx site, including most Read the Docs projects. MDN also renders its search client-side, but the page gets its results from a public JSON endpoint, so `oc mdn search` asks that endpoint directly and prints the site's own ranking; that `api` shape in a site definition works for any site whose search answers as JSON. Node.js ships no search endpoint at all, but publishes its whole API reference as one static JSON file, so `oc node search` ranks that file locally the same way the Sphinx backend does, under the same day cache, and every module, class, method, property, and event heading links to its own anchor. Ruby's docs are built with RDoc, which also ships its search index as one static file, so `oc ruby search` ranks every class, method, and guide page locally the same way. PHP's manual has a lookup endpoint that sends an exact function name straight to its page, which is what `oc php fn` rides. Not supported yet: pages that only render with JavaScript, sites behind logins, and sites with hard bot challenges that expose no feed. Want a website on that list? Open a pull request, or an issue naming the site; see [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/clis/doc.rust-lang.org.json b/clis/doc.rust-lang.org.json new file mode 100644 index 0000000..4672ec7 --- /dev/null +++ b/clis/doc.rust-lang.org.json @@ -0,0 +1,8 @@ +{ + "domain": "doc.rust-lang.org", + "commands": { + "std": { "open": "https://doc.rust-lang.org/std/{path}.html", "args": ["path"] }, + "doc": { "open": "https://doc.rust-lang.org/{path}.html", "args": ["path"] }, + "search": { "open": "https://html.duckduckgo.com/html/?q=site%3Adoc.rust-lang.org+{query}", "args": ["query"] } + } +} diff --git a/clis/docs.oracle.com.json b/clis/docs.oracle.com.json new file mode 100644 index 0000000..66e4cf6 --- /dev/null +++ b/clis/docs.oracle.com.json @@ -0,0 +1,7 @@ +{ + "domain": "docs.oracle.com", + "commands": { + "api": { "open": "https://docs.oracle.com/en/java/javase/26/docs/api/{path}.html", "args": ["path"] }, + "search": { "open": "https://html.duckduckgo.com/html/?q=site%3Adocs.oracle.com+javase+{query}", "args": ["query"] } + } +} diff --git a/clis/en.cppreference.com.json b/clis/en.cppreference.com.json new file mode 100644 index 0000000..855563e --- /dev/null +++ b/clis/en.cppreference.com.json @@ -0,0 +1,8 @@ +{ + "domain": "en.cppreference.com", + "commands": { + "cpp": { "open": "https://en.cppreference.com/cpp/{path}", "args": ["path"] }, + "c": { "open": "https://en.cppreference.com/c/{path}", "args": ["path"] }, + "search": { "open": "https://html.duckduckgo.com/html/?q=site%3Aen.cppreference.com+{query}", "args": ["query"] } + } +} diff --git a/clis/learn.microsoft.com.json b/clis/learn.microsoft.com.json index 7e1e551..d532b96 100644 --- a/clis/learn.microsoft.com.json +++ b/clis/learn.microsoft.com.json @@ -4,6 +4,7 @@ "azure": { "open": "https://learn.microsoft.com/en-us/azure/{page}", "args": ["page"] }, "doc": { "open": "https://learn.microsoft.com/en-us/{path}", "args": ["path"] }, "cli": { "open": "https://learn.microsoft.com/en-us/cli/azure/{command}", "args": ["command"] }, + "dotnet": { "open": "https://learn.microsoft.com/en-us/dotnet/api/{api}", "args": ["api"] }, "search": { "open": "https://learn.microsoft.com/api/search/rss?search={query}&locale=en-us", "args": ["query"] } } } diff --git a/clis/php.net.json b/clis/php.net.json new file mode 100644 index 0000000..3633b34 --- /dev/null +++ b/clis/php.net.json @@ -0,0 +1,8 @@ +{ + "domain": "php.net", + "commands": { + "fn": { "open": "https://www.php.net/manual-lookup.php?pattern={name}", "args": ["name"] }, + "doc": { "open": "https://www.php.net/manual/en/{path}.php", "args": ["path"] }, + "search": { "open": "https://html.duckduckgo.com/html/?q=site%3Aphp.net+{query}", "args": ["query"] } + } +} diff --git a/clis/pkg.go.dev.json b/clis/pkg.go.dev.json new file mode 100644 index 0000000..ffc070d --- /dev/null +++ b/clis/pkg.go.dev.json @@ -0,0 +1,7 @@ +{ + "domain": "pkg.go.dev", + "commands": { + "pkg": { "open": "https://pkg.go.dev/{path}", "args": ["path"] }, + "search": { "open": "https://pkg.go.dev/search?q={query}", "args": ["query"] } + } +} diff --git a/clis/typescriptlang.org.json b/clis/typescriptlang.org.json new file mode 100644 index 0000000..847083f --- /dev/null +++ b/clis/typescriptlang.org.json @@ -0,0 +1,7 @@ +{ + "domain": "typescriptlang.org", + "commands": { + "handbook": { "open": "https://www.typescriptlang.org/docs/handbook/{page}.html", "args": ["page"] }, + "search": { "open": "https://html.duckduckgo.com/html/?q=site%3Atypescriptlang.org+{query}", "args": ["query"] } + } +} diff --git a/skills/web-browsing-cli/SKILL.md b/skills/web-browsing-cli/SKILL.md index ca42984..ef6ae94 100644 --- a/skills/web-browsing-cli/SKILL.md +++ b/skills/web-browsing-cli/SKILL.md @@ -21,7 +21,7 @@ None of these except `open`/`do`/`raw <url>` fetch anything; they replay the pag ## Site shortcuts -`oc <site> <verb> [args]` resolves to a URL and then behaves exactly like `open` on it, so it costs the same and reads the same. It saves guessing a URL shape and, on a few sites, points at the feed or public API that answers without a login. Three verbs are not URLs: `oc py search <query>` ranks the Python docs' own search index locally, `oc node search <query>` does the same over the Node.js docs' JSON reference, and `oc mdn search <query>` asks MDN's own search API; all three print results as a normal numbered page. +`oc <site> <verb> [args]` resolves to a URL and then behaves exactly like `open` on it, so it costs the same and reads the same. It saves guessing a URL shape and, on a few sites, points at the feed or public API that answers without a login. Four verbs are not URLs: `oc py search <query>` ranks the Python docs' own search index locally, `oc node search <query>` does the same over the Node.js docs' JSON reference, `oc ruby search <query>` does it over the Ruby docs' RDoc index, and `oc mdn search <query>` asks MDN's own search API; all four print results as a normal numbered page. ``` oc hn top oc reddit sub ClaudeAI oc gh repo only-cli oc @@ -30,7 +30,7 @@ oc ddg search claude code oc so question 231767 oc learn doc azur oc py library json oc mdn js Array/map oc node api fs ``` -Sites: `hn`, `reddit`, `gh`, `x`, `linkedin`, `ddg`, `bing`, `so`, `finance`, `yt`, `aws`, `gcp`, `learn`, `wiki`, `py`, `mdn`, `node`. Name one by short name, bare name, or domain (`oc hn`, `oc ycombinator`, `oc news.ycombinator.com`). The last argument takes every word after it, so a query or title needs no quoting. `oc sites` lists every site with its verbs, which is cheaper than guessing one. +Sites: `hn`, `reddit`, `gh`, `x`, `linkedin`, `ddg`, `bing`, `so`, `finance`, `yt`, `aws`, `gcp`, `learn`, `wiki`, `py`, `mdn`, `node`, `ruby`, `go`, `rust`, `java`, `php`, `cpp`, `ts`. Name one by short name, bare name, or domain (`oc hn`, `oc ycombinator`, `oc news.ycombinator.com`). The last argument takes every word after it, so a query or title needs no quoting. `oc sites` lists every site with its verbs, which is cheaper than guessing one. Prefer a shortcut over a hand-built URL when one exists for the site, and prefer `oc wiki article <title>` over a search when you already know the article's name. diff --git a/tests/sites.test.js b/tests/sites.test.js index ed20a6d..267d9e6 100644 --- a/tests/sites.test.js +++ b/tests/sites.test.js @@ -56,7 +56,7 @@ test('every shipped definition is reachable and every url template is filled', ( // A search verb resolves to a site root or endpoint to ask, not a // URL: an API endpoint keeps {query} until search time, so it is // filled here the way apiSearch fills it before the template check. - const url = resolved.url ?? resolved.sphinx ?? resolved.nodedoc + const url = resolved.url ?? resolved.sphinx ?? resolved.nodedoc ?? resolved.rdoc ?? (def.args ?? []).reduce((u, a) => u.replaceAll(`{${a}}`, `test-${a}`), resolved.api?.api ?? ''); assert.doesNotMatch(url, /[{}]/, `oc ${name} ${verb} left a template var in ${url}`); assert.equal(new URL(url).protocol, 'https:', `oc ${name} ${verb} is not https`); @@ -99,3 +99,37 @@ test('language docs shortcuts resolve, and a doc path keeps its slashes', () => assert.equal(mdn.api.api, 'https://developer.mozilla.org/api/v1/search?q={query}&locale=en-US'); assert.equal(mdn.query, 'array map'); }); + +test('the second wave of language docs resolves the same way', () => { + assert.equal(resolveSite('go', ['pkg', 'net/http']).url, 'https://pkg.go.dev/net/http'); + assert.equal( + resolveSite('go', ['search', 'json decode']).url, + 'https://pkg.go.dev/search?q=json%20decode'); + assert.equal( + resolveSite('php', ['fn', 'array_map']).url, + 'https://www.php.net/manual-lookup.php?pattern=array_map'); + assert.equal( + resolveSite('cpp', ['cpp', 'container/vector']).url, + 'https://en.cppreference.com/cpp/container/vector'); + assert.equal( + resolveSite('cppreference', ['search', 'push_back']).url, + 'https://html.duckduckgo.com/html/?q=site%3Aen.cppreference.com+push_back'); + assert.equal( + resolveSite('rust', ['std', 'vec/struct.Vec']).url, + 'https://doc.rust-lang.org/std/vec/struct.Vec.html'); + assert.equal( + resolveSite('java', ['api', 'java.base/java/util/HashMap']).url, + 'https://docs.oracle.com/en/java/javase/26/docs/api/java.base/java/util/HashMap.html'); + assert.equal( + resolveSite('ts', ['handbook', '2/everyday-types']).url, + 'https://www.typescriptlang.org/docs/handbook/2/everyday-types.html'); + assert.equal( + resolveSite('learn', ['dotnet', 'system.string']).url, + 'https://learn.microsoft.com/en-us/dotnet/api/system.string'); + assert.equal( + resolveSite('ruby', ['class', 'Array']).url, + 'https://docs.ruby-lang.org/en/3.4/Array.html'); + const ruby = resolveSite('docs.ruby-lang.org', ['search', 'each_slice']); + assert.equal(ruby.rdoc, 'https://docs.ruby-lang.org/en/3.4/'); + assert.equal(ruby.query, 'each_slice'); +});