website-api 1.1.1 → 1.1.2

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/bin/cli.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{readFileSync as e,writeFileSync as o}from"node:fs";import{dirname as n,join as s}from"node:path";import{fileURLToPath as t}from"node:url";import{program as r}from"commander";import i from"chalk";import l from"cli-table3";import{getDefaultChromeDir as a}from"chrome-tools";import{queryWebsite as c,sites as p,loadSites as d,getSite as u}from"../src/website-api.js";import{parseArgsForWebsite as g}from"../src/util/args-parser.js";const f=s(n(t(import.meta.url)),"..","..","package.json"),{version:m}=JSON.parse(e(f,"utf8"));process.on("unhandledRejection",e=>{console.error(e instanceof Error?e.message:"command not found"),process.exit(1)}),async function(){const e=process.argv.slice(2),n=e.find(e=>!e.startsWith("-"));if(n&&"list"!==n){await d();const s=u(n);if(s){const t=e.filter((o,s)=>s!==e.indexOf(n));let r;try{r=g(s.positionals,s.parameters,t)}catch(e){console.error(i.red(`Error: ${e instanceof Error?e.message:String(e)}`)),console.log(`Run ${i.cyan(`npx website-api ${s.id} --help`)} for usage details.`),process.exit(1)}if(r.helpRequested)return void function(e){console.log(i.bold.green(`\n🌐 Website API: ${i.white(e.name)} (${i.yellow(e.id)})\n`)),console.log(` ${i.italic(e.description)}\n`);let o=`npx website-api ${e.id}`;if(e.positionals&&e.positionals.length>0)for(const n of e.positionals)o+=n.required?` <${n.name}>`:` [${n.name}]`;if(o+=" [options]",console.log(`${i.bold("Usage:")} ${i.cyan(o)}\n`),e.positionals&&e.positionals.length>0){console.log(i.bold("Positional Arguments:"));for(const o of e.positionals)console.log(` ${i.cyan(o.name.padEnd(15))} ${o.description}`);console.log()}console.log(i.bold("Options:"));const n=[...e.parameters||[],{name:"profile",type:"string",description:"specific Chrome profile directory (e.g., 'Default')"},{name:"user-agent",type:"string",description:"custom User-Agent header for HTTP requests",short:"u"},{name:"debug",type:"boolean",description:"Print full HTTP request and response bodies for debugging"},{name:"keep-open",type:"boolean",description:"Leave the browser tab open after running (preserve the logged-in session)"},{name:"help",type:"boolean",description:"Show help for this website site",short:"h"}];for(const e of n){let o=`--${e.name}`;"boolean"!==e.type&&(o+=" <value>"),o=e.short?`-${e.short}, ${o}`:` ${o}`;const n=void 0!==e.default?` (default: ${e.default})`:"";console.log(` ${i.yellow(o.padEnd(28))} ${e.description}${i.gray(n)}`)}console.log()}(s);try{const e=await c(s.id,r.options);let n;if(r.options.text&&e&&"object"==typeof e){const o=e.answer||e.text;n=void 0!==o?String(o):JSON.stringify(e,null,2)}else n="string"==typeof e?e:JSON.stringify(e,null,2);r.options.out?(o(r.options.out,n+"\n","utf8"),console.log(i.green(`Success! Decoded response written to ${r.options.out}`))):console.log(n)}catch(e){console.error(i.red(e instanceof Error?e.message:"command not found")),process.exit(1)}return}}r.name("website-api").description("CLI to query website APIs using decrypted Chrome cookies on macOS").version(m),r.option("--profile <name>","specific Chrome profile directory (e.g., 'Default', 'Profile 1')").option("--current-profile","Show the currently resolved/selected Chrome profile directory and name").option("-u, --user-agent <string>","custom User-Agent header for HTTP requests"),r.option("--debug","Print full HTTP request and response bodies for debugging"),r.command("list").description("List all supported website API sites").action(async()=>{await d(),console.log(i.bold.green("\n🌐 Supported Website APIs:\n"));const e=new l({head:[i.bold.cyan("ID"),i.bold.cyan("Name"),i.bold.cyan("Domain"),i.bold.cyan("Description")],colWidths:[18,25,20,50],wordWrap:!0,style:{head:[],border:[]}});for(const o of p){const n=[];"browser"===o.transport&&n.push(i.magenta("[p]")),o.auth&&n.push(i.red("[l]")),"extension"===o.origin&&n.push(i.blue("[x]"));const s=n.length?`${o.name} ${n.join(" ")}`:o.name;e.push([i.yellow(o.id),s,i.underline(o.domain),o.description])}console.log(e.toString()),console.log(`\n${i.magenta("[p]")} requires a running Chrome (Playwright) ${i.red("[l]")} requires login ${i.blue("[x]")} user extension`),console.log(`\nTo run an API query, execute: ${i.bold.cyan("npx website-api <id>")}\n`)}),r.argument("[website]","website ID or domain to query (e.g. 'chatgpt.com')").action(async e=>{const o=r.opts();if(o.currentProfile){const e=process.env.PROFILE_PATH||process.env.CHROME_PROFILE_PATH||a(),n=o.profile||process.env.PROFILE_NAME||"Default";return console.log(i.bold.green("\nšŸ‘¤ Currently Resolved Profile:\n")),console.log(` ${i.bold("Path:")} ${e}`),void console.log(` ${i.bold("Name:")} ${n}\n`)}e?(console.error(i.red(`Error: website adapter "${e}" not found.`)),console.log(`Run ${i.cyan("npx website-api list")} to see all supported adapters.`),process.exit(1)):r.outputHelp()}),r.parse(process.argv)}().catch(e=>{console.error(e instanceof Error?e.message:"command not found"),process.exit(1)});
2
+ import{readFileSync as e,writeFileSync as o}from"node:fs";import{dirname as n,join as s}from"node:path";import{fileURLToPath as t}from"node:url";import{program as r}from"commander";import i from"chalk";import l from"cli-table3";import{getDefaultChromeDir as a}from"chrome-tools";import{queryWebsite as c,sites as p,loadSites as d,getSite as u}from"../src/website-api.js";import{registerExtCommands as g}from"../src/cli/ext.js";import{parseArgsForWebsite as f}from"../src/util/args-parser.js";const m=s(n(t(import.meta.url)),"..","..","package.json"),{version:b}=JSON.parse(e(m,"utf8"));process.on("unhandledRejection",e=>{console.error(e instanceof Error?e.message:"command not found"),process.exit(1)}),async function(){const e=process.argv.slice(2),n=e.find(e=>!e.startsWith("-"));if(n&&"list"!==n){await d();const s=u(n);if(s){const t=e.filter((o,s)=>s!==e.indexOf(n));let r;try{r=f(s.positionals,s.parameters,t)}catch(e){console.error(i.red(`Error: ${e instanceof Error?e.message:String(e)}`)),console.log(`Run ${i.cyan(`npx website-api ${s.id} --help`)} for usage details.`),process.exit(1)}if(r.helpRequested)return void function(e){console.log(i.bold.green(`\n🌐 Website API: ${i.white(e.name)} (${i.yellow(e.id)})\n`)),console.log(` ${i.italic(e.description)}\n`);let o=`npx website-api ${e.id}`;if(e.positionals&&e.positionals.length>0)for(const n of e.positionals)o+=n.required?` <${n.name}>`:` [${n.name}]`;if(o+=" [options]",console.log(`${i.bold("Usage:")} ${i.cyan(o)}\n`),e.positionals&&e.positionals.length>0){console.log(i.bold("Positional Arguments:"));for(const o of e.positionals)console.log(` ${i.cyan(o.name.padEnd(15))} ${o.description}`);console.log()}console.log(i.bold("Options:"));const n=[...e.parameters||[],{name:"profile",type:"string",description:"specific Chrome profile directory (e.g., 'Default')"},{name:"user-agent",type:"string",description:"custom User-Agent header for HTTP requests",short:"u"},{name:"debug",type:"boolean",description:"Print full HTTP request and response bodies for debugging"},{name:"keep-open",type:"boolean",description:"Leave the browser tab open after running (preserve the logged-in session)"},{name:"help",type:"boolean",description:"Show help for this website site",short:"h"}];for(const e of n){let o=`--${e.name}`;"boolean"!==e.type&&(o+=" <value>"),o=e.short?`-${e.short}, ${o}`:` ${o}`;const n=void 0!==e.default?` (default: ${e.default})`:"";console.log(` ${i.yellow(o.padEnd(28))} ${e.description}${i.gray(n)}`)}console.log()}(s);try{const e=await c(s.id,r.options);let n;if(r.options.text&&e&&"object"==typeof e){const o=e.answer||e.text;n=void 0!==o?String(o):JSON.stringify(e,null,2)}else n="string"==typeof e?e:JSON.stringify(e,null,2);r.options.out?(o(r.options.out,n+"\n","utf8"),console.log(i.green(`Success! Decoded response written to ${r.options.out}`))):console.log(n)}catch(e){console.error(i.red(e instanceof Error?e.message:"command not found")),process.exit(1)}return}}r.name("website-api").description("CLI to query website APIs using decrypted Chrome cookies on macOS").version(b),r.option("--profile <name>","specific Chrome profile directory (e.g., 'Default', 'Profile 1')").option("--current-profile","Show the currently resolved/selected Chrome profile directory and name").option("-u, --user-agent <string>","custom User-Agent header for HTTP requests"),r.option("--debug","Print full HTTP request and response bodies for debugging"),r.command("list").description("List all supported website API sites").action(async()=>{await d(),console.log(i.bold.green("\n🌐 Supported Website APIs:\n"));const e=new l({head:[i.bold.cyan("ID"),i.bold.cyan("Name"),i.bold.cyan("Domain"),i.bold.cyan("Description")],colWidths:[18,25,20,50],wordWrap:!0,style:{head:[],border:[]}});for(const o of p){const n=[];"browser"===o.transport&&n.push(i.magenta("[p]")),o.auth&&n.push(i.red("[l]")),"extension"===o.origin&&n.push(i.blue("[x]"));const s=n.length?`${o.name} ${n.join(" ")}`:o.name;e.push([i.yellow(o.id),s,i.underline(o.domain),o.description])}console.log(e.toString()),console.log(`\n${i.magenta("[p]")} requires a running Chrome (Playwright) ${i.red("[l]")} requires login ${i.blue("[x]")} user extension`),console.log(`\nTo run an API query, execute: ${i.bold.cyan("npx website-api <id>")}\n`)}),g(r),r.argument("[website]","website ID or domain to query (e.g. 'chatgpt.com')").action(async e=>{const o=r.opts();if(o.currentProfile){const e=process.env.PROFILE_PATH||process.env.CHROME_PROFILE_PATH||a(),n=o.profile||process.env.PROFILE_NAME||"Default";return console.log(i.bold.green("\nšŸ‘¤ Currently Resolved Profile:\n")),console.log(` ${i.bold("Path:")} ${e}`),void console.log(` ${i.bold("Name:")} ${n}\n`)}e?(console.error(i.red(`Error: website adapter "${e}" not found.`)),console.log(`Run ${i.cyan("npx website-api list")} to see all supported adapters.`),process.exit(1)):r.outputHelp()}),r.parse(process.argv)}().catch(e=>{console.error(e instanceof Error?e.message:"command not found"),process.exit(1)});
@@ -0,0 +1,3 @@
1
+ import type { Command } from "commander";
2
+ /** Registers the `ext` command group on the given Commander program. */
3
+ export declare function registerExtCommands(program: Command): void;
@@ -0,0 +1 @@
1
+ import{createInterface as e}from"node:readline/promises";import{stdin as o,stdout as n}from"node:process";import i from"chalk";import t from"cli-table3";import{loadSites as s,getSite as r}from"../core/runtime.js";import{addRegistry as a,installEntry as l,listInstalled as c,loadIndex as d,removeInstalled as g,removeRegistry as m,resolveEntry as y,resolveRegistries as p,searchRegistries as h}from"../core/registry.js";async function f(t){if(!o.isTTY)return!1;const s=e({input:o,output:n});try{const e=(await s.question(`${t} ${i.gray("[y/N]")} `)).trim().toLowerCase();return"y"===e||"yes"===e}finally{s.close()}}function u(e){console.error(i.red(`Error: ${e}`)),process.exit(1)}export function registerExtCommands(e){const o=e.command("ext").description("Discover and install website extensions from a public registry");o.command("search [query]").description("Search configured registries for installable sites").option("--refresh","Bypass the cached index and re-fetch").action(async(e,o)=>{const n=await h(e??"",{refresh:o.refresh});if(0===n.length)return void console.log(i.yellow(`No sites${e?` matching "${e}"`:""} found.`));const s=new t({head:[i.bold.cyan("ID"),i.bold.cyan("Name"),i.bold.cyan("Domain"),i.bold.cyan("Registry"),i.bold.cyan("Description")],colWidths:[16,22,18,14,44],wordWrap:!0,style:{head:[],border:[]}});for(const e of n){const o=[];"browser"===e.transport&&o.push(i.magenta("[p]")),e.auth&&o.push(i.red("[l]"));const n=o.length?`${e.name} ${o.join(" ")}`:e.name;s.push([i.yellow(e.id),n,i.underline(e.domain),e.registry.name,e.description])}console.log(s.toString()),console.log(`\nInstall one with: ${i.bold.cyan("npx website-api ext install <id>")}\n`)}),o.command("info <id>").description("Show full catalog details for a single site").option("--registry <name>","Disambiguate when multiple registries offer the id").action(async(e,o)=>{const{source:n,entry:t}=await y(e,{registryName:o.registry});console.log(i.bold.green(`\n🌐 ${t.name} ${i.yellow(`(${t.id})`)}\n`)),console.log(` ${i.italic(t.description)}\n`);const s=[["Domain",t.domain],["Registry",`${n.name} (${n.repo})`],["Version",t.version??"—"],["Transport",t.transport??"http"],["Requires login",t.auth?"yes":"no"],["Tags",t.tags?.join(", ")||"—"],["Files",t.files.map(e=>e.name).join(", ")]];for(const[e,o]of s)console.log(` ${i.bold((e+":").padEnd(16))} ${o}`);console.log(`\nInstall with: ${i.bold.cyan(`npx website-api ext install ${t.id}`)}\n`)}),o.command("install <id>").alias("add-site").description("Download and install a site from a registry into your extensions folder").option("--registry <name>","Disambiguate when multiple registries offer the id").option("-y, --yes","Skip the confirmation prompt").option("--refresh","Bypass the cached index and re-fetch").action(async(e,o)=>{const{source:n,index:t,entry:a}=await y(e,{registryName:o.registry,refresh:o.refresh});console.log(i.bold(`\nAbout to install ${i.yellow(a.id)} — ${a.name}`)),console.log(` ${i.bold("Domain:")} ${a.domain}`),console.log(` ${i.bold("Registry:")} ${n.name} (${n.repo}@${t.commit.slice(0,10)})`),console.log(` ${i.bold("Files:")} ${a.files.map(e=>e.name).join(", ")}`),a.auth&&console.log(i.red(` ⚠ This site performs a login and will read saved credentials for ${a.domain}.`)),await s();const c=r(a.id);if("bundled"===c?.origin&&console.log(i.yellow(` ⚠ This will shadow the bundled "${a.id}" site.`)),console.log(i.gray(" ⚠ Installing runs third-party code with your browser session. Only install sites you trust.\n")),!o.yes&&!await f("Install this site?"))return void console.log(i.yellow("Aborted."));const{dir:d}=await l(n,t,a,{refresh:o.refresh});console.log(i.green(`\nāœ“ Installed ${a.id} → ${d}`)),console.log(`Run it with: ${i.bold.cyan(`npx website-api ${a.id}`)}\n`)}),o.command("list").description("List sites you have installed from registries").action(()=>{const e=c();if(0===e.length)return void console.log(i.yellow("No registry sites installed. Try: ")+i.cyan("npx website-api ext search"));const o=new t({head:[i.bold.cyan("ID"),i.bold.cyan("Version"),i.bold.cyan("Registry"),i.bold.cyan("Commit"),i.bold.cyan("Installed")],style:{head:[],border:[]}});for(const n of e)o.push([i.yellow(n.id),n.version??"—",n.repo,n.commit.slice(0,10),n.installedAt.slice(0,10)]);console.log(o.toString())}),o.command("remove <id>").alias("uninstall").description("Remove an installed registry site").action(e=>{g(e)?console.log(i.green(`āœ“ Removed ${e}`)):u(`"${e}" is not installed`)}),o.command("update [id]").description("Re-install installed sites whose registry commit has changed").option("-y, --yes","Skip the confirmation prompt").action(async(e,o)=>{const n=c().filter(o=>!e||o.id===e);0===n.length&&u(e?`"${e}" is not installed`:"no registry sites installed");let t=0;for(const e of n){let n;try{n=await y(e.id,{registryName:e.registry,refresh:!0})}catch{console.log(i.yellow(`• ${e.id}: no longer in registry, skipping`));continue}n.index.commit!==e.commit?(console.log(`• ${e.id}: ${e.commit.slice(0,10)} → ${n.index.commit.slice(0,10)}`),(o.yes||await f(` Update ${e.id}?`))&&(await l(n.source,n.index,n.entry),console.log(i.green(` āœ“ updated ${e.id}`)),t++)):console.log(i.gray(`• ${e.id}: up to date`))}console.log(t?i.green(`\nUpdated ${t} site(s).`):i.gray("\nNothing to update."))});const n=o.command("registry").description("Manage the registries searched for sites");n.command("list").description("List configured registries (in search priority order)").action(()=>{const e=new t({head:[i.bold.cyan("Name"),i.bold.cyan("Repo"),i.bold.cyan("Branch")],style:{head:[],border:[]}});for(const o of p())e.push([o.name,o.repo,o.branch]);console.log(e.toString())}),n.command("add <spec>").description("Add a registry (owner/repo, owner/repo#branch, or a github.com URL)").action(async e=>{const o=a(e);try{const e=await d(o,{refresh:!0});console.log(i.green(`āœ“ Added registry ${o.name} (${o.repo}) — ${e.sites.length} site(s) available`))}catch(e){console.log(i.yellow(`Added ${o.repo}, but its index.json could not be fetched: ${e instanceof Error?e.message:String(e)}`))}}),n.command("remove <repoOrName>").description("Remove a configured registry").action(e=>{m(e)?console.log(i.green(`āœ“ Removed registry ${e}`)):u(`registry "${e}" not found in config`)})}
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Remote site registry ("a public directory of installable sites").
3
+ *
4
+ * A registry is just a public Git repo containing a generated `index.json`
5
+ * catalog plus one folder of prebuilt JS per site. Nothing here touches the
6
+ * loader: `install` only downloads files into the extensions root that
7
+ * {@link extensionRoots} already discovers, so an installed site behaves
8
+ * exactly like any hand-placed extension.
9
+ */
10
+ /** A configured registry source. `repo` is "owner/name". */
11
+ export interface RegistrySource {
12
+ name: string;
13
+ repo: string;
14
+ branch: string;
15
+ }
16
+ /** One file of a site, with its expected content hash for integrity. */
17
+ export interface RegistryFile {
18
+ name: string;
19
+ sha256: string;
20
+ }
21
+ /** One catalog entry — mirrors the public Site fields plus install metadata. */
22
+ export interface RegistrySiteEntry {
23
+ id: string;
24
+ name: string;
25
+ domain: string;
26
+ description: string;
27
+ version?: string;
28
+ transport?: "http" | "browser";
29
+ auth?: boolean;
30
+ tags?: string[];
31
+ /** Folder within the repo holding this site's files. */
32
+ path: string;
33
+ /** Files to download; first entry is treated as the module entry point. */
34
+ files: RegistryFile[];
35
+ }
36
+ /** The generated catalog served at the registry repo root. */
37
+ export interface RegistryIndex {
38
+ schemaVersion: number;
39
+ /** Commit the `files[].sha256` hashes were generated against. */
40
+ commit: string;
41
+ updatedAt?: string;
42
+ sites: RegistrySiteEntry[];
43
+ }
44
+ /** Provenance written next to an installed site as `.source.json`. */
45
+ export interface InstalledRecord {
46
+ id: string;
47
+ registry: string;
48
+ repo: string;
49
+ commit: string;
50
+ version?: string;
51
+ installedAt: string;
52
+ }
53
+ /** A catalog entry tagged with the registry it came from. */
54
+ export type FoundEntry = RegistrySiteEntry & {
55
+ registry: RegistrySource;
56
+ };
57
+ /** Pluggable fetch so tests never hit the network. */
58
+ export type FetchImpl = (url: string) => Promise<{
59
+ ok: boolean;
60
+ status: number;
61
+ arrayBuffer(): Promise<ArrayBuffer>;
62
+ text(): Promise<string>;
63
+ }>;
64
+ /** Built-in default registry, used when the user has configured none. */
65
+ export declare const DEFAULT_REGISTRY: RegistrySource;
66
+ interface RegistryOpts {
67
+ env?: NodeJS.ProcessEnv;
68
+ fetchImpl?: FetchImpl;
69
+ refresh?: boolean;
70
+ ttlMs?: number;
71
+ }
72
+ /** `~/.config/website-api/config.json` */
73
+ export declare function configPath(env?: NodeJS.ProcessEnv): string;
74
+ /** Parses "owner/repo", "owner/repo#branch", or a github.com URL. */
75
+ export declare function parseRepoSpec(spec: string): RegistrySource;
76
+ /**
77
+ * The registries to search, in priority order:
78
+ * 1. $WEBSITE_API_REGISTRY (one-off override; may be a comma list)
79
+ * 2. configured registries in config.json
80
+ * 3. the built-in DEFAULT_REGISTRY (always present as a fallback)
81
+ * Later duplicates by `repo` are dropped.
82
+ */
83
+ export declare function resolveRegistries(env?: NodeJS.ProcessEnv): RegistrySource[];
84
+ /** Adds a registry to config.json (idempotent by repo). Returns the source. */
85
+ export declare function addRegistry(spec: string, env?: NodeJS.ProcessEnv): RegistrySource;
86
+ /** Removes a registry from config.json by repo or name. Returns true if removed. */
87
+ export declare function removeRegistry(repoOrName: string, env?: NodeJS.ProcessEnv): boolean;
88
+ /** Loads (and disk-caches with a TTL) one registry's index.json. */
89
+ export declare function loadIndex(source: RegistrySource, opts?: RegistryOpts): Promise<RegistryIndex>;
90
+ /** Searches every configured registry; optional free-text query over id/name/domain/tags. */
91
+ export declare function searchRegistries(query: string, opts?: RegistryOpts): Promise<FoundEntry[]>;
92
+ /**
93
+ * Resolves a single id to its catalog entry. If `registryName` is omitted and
94
+ * more than one registry offers the id, throws and asks the caller to choose.
95
+ */
96
+ export declare function resolveEntry(id: string, opts?: RegistryOpts & {
97
+ registryName?: string;
98
+ }): Promise<{
99
+ source: RegistrySource;
100
+ index: RegistryIndex;
101
+ entry: RegistrySiteEntry;
102
+ }>;
103
+ /**
104
+ * Downloads one site's files (pinned to `index.commit`), verifies each against
105
+ * its sha256, writes them to the extensions root, validates the entry actually
106
+ * loads as a site, and records provenance. Returns the install directory.
107
+ */
108
+ export declare function installEntry(source: RegistrySource, index: RegistryIndex, entry: RegistrySiteEntry, opts?: RegistryOpts): Promise<{
109
+ dir: string;
110
+ record: InstalledRecord;
111
+ }>;
112
+ /** Lists locally installed registry sites (those carrying a `.source.json`). */
113
+ export declare function listInstalled(env?: NodeJS.ProcessEnv): InstalledRecord[];
114
+ /** Removes an installed site directory. Returns true if something was removed. */
115
+ export declare function removeInstalled(id: string, env?: NodeJS.ProcessEnv): boolean;
116
+ export {};
@@ -0,0 +1 @@
1
+ import{createHash as e}from"node:crypto";import{existsSync as t,mkdirSync as r,readdirSync as n,readFileSync as o,rmSync as i,statSync as s,writeFileSync as c}from"node:fs";import{homedir as a}from"node:os";import{dirname as f,join as p}from"node:path";import{pathToFileURL as u}from"node:url";import{defineSite as l}from"./define-site.js";import{extensionRoots as d}from"./loader.js";export const DEFAULT_REGISTRY={name:"guocity",repo:"guocity/website-api-list",branch:"main"};function h(e){return e.XDG_CONFIG_HOME||p(a(),".config")}export function configPath(e=process.env){return p(h(e),"website-api","config.json")}function m(e){return d(e)[0]}export function parseRepoSpec(e){let t=e.trim();t=t.replace(/^https?:\/\/github\.com\//i,"").replace(/^github:/i,""),t=t.replace(/\.git$/i,"").replace(/\/$/,"");let r="main";const n=t.indexOf("#");n>=0&&(r=t.slice(n+1)||"main",t=t.slice(0,n));const o=t.split("/").filter(Boolean);if(o.length<2)throw new Error(`Invalid registry spec "${e}" (expected owner/repo)`);const i=`${o[0]}/${o[1]}`;return{name:o[0],repo:i,branch:r}}function g(e){const r=configPath(e);if(!t(r))return{};try{return JSON.parse(o(r,"utf8"))}catch{return{}}}function w(e,t){const n=configPath(t);r(f(n),{recursive:!0}),c(n,JSON.stringify(e,null,2)+"\n","utf8")}export function resolveRegistries(e=process.env){const t=[],r=new Set,n=e=>{r.has(e.repo)||(r.add(e.repo),t.push(e))};if(e.WEBSITE_API_REGISTRY)for(const t of e.WEBSITE_API_REGISTRY.split(","))t.trim()&&n(parseRepoSpec(t));for(const t of g(e).registries??[])n(t);return n(DEFAULT_REGISTRY),t}export function addRegistry(e,t=process.env){const r=parseRepoSpec(e),n=g(t);return n.registries=(n.registries??[]).filter(e=>e.repo!==r.repo),n.registries.push(r),w(n,t),r}export function removeRegistry(e,t=process.env){const r=g(t),n=r.registries?.length??0;return r.registries=(r.registries??[]).filter(t=>t.repo!==e&&t.name!==e),w(r,t),(r.registries?.length??0)<n}function y(e,t,r){return`https://raw.githubusercontent.com/${e}/${t}/${r}`}function v(){return e=>fetch(e)}async function $(e,t){const r=await t(e);if(!r.ok)throw new Error(`GET ${e} → HTTP ${r.status}`);return Buffer.from(await r.arrayBuffer())}function E(t){return e("sha256").update(t).digest("hex")}export async function loadIndex(e,n={}){const i=n.env??process.env,s=n.fetchImpl??v(),a=n.ttlMs??36e5,u=function(e,t){const r=e.replace(/[^a-z0-9._-]+/gi,"_");return p(h(t),"website-api","cache",`${r}.json`)}(e.repo,i);if(!n.refresh&&t(u))try{const{fetchedAt:e,index:t}=JSON.parse(o(u,"utf8"));if("number"==typeof e&&Date.now()-e<a)return t}catch{}const l=await async function(e,t){const r=await t(e);if(!r.ok)throw new Error(`GET ${e} → HTTP ${r.status}`);return r.text()}(y(e.repo,e.branch,"index.json"),s),d=JSON.parse(l);if(!d||!Array.isArray(d.sites))throw new Error(`Registry ${e.repo} has no valid index.json`);try{r(f(u),{recursive:!0}),c(u,JSON.stringify({fetchedAt:Date.now(),index:d}),"utf8")}catch{}return d}export async function searchRegistries(e,t={}){const r=t.env??process.env,n=e.trim().toLowerCase(),o=[];for(const e of resolveRegistries(r)){let r;try{r=await loadIndex(e,t)}catch{continue}for(const t of r.sites){const r=[t.id,t.name,t.domain,t.description,...t.tags??[]].join(" ").toLowerCase();n&&!r.includes(n)||o.push({...t,registry:e})}}return o}export async function resolveEntry(e,t={}){const r=t.env??process.env,n=[];for(const o of resolveRegistries(r)){if(t.registryName&&o.name!==t.registryName&&o.repo!==t.registryName)continue;let r;try{r=await loadIndex(o,t)}catch{continue}const i=r.sites.find(t=>t.id===e);i&&n.push({source:o,index:r,entry:i})}if(0===n.length)throw new Error(`Site "${e}" not found in any registry`);if(n.length>1){const t=n.map(e=>e.source.name).join(", ");throw new Error(`Site "${e}" is offered by multiple registries (${t}); pass --registry <name>`)}return n[0]}export async function installEntry(e,t,n,o={}){const s=o.env??process.env,a=o.fetchImpl??v();if(!n.files?.length)throw new Error(`Catalog entry "${n.id}" lists no files`);const d=p(m(s),n.id),h=[];for(const r of n.files){const o=y(e.repo,t.commit,`${n.path}/${r.name}`),i=await $(o,a),s=E(i);if(s!==r.sha256)throw new Error(`Integrity check failed for ${n.id}/${r.name} (expected ${r.sha256.slice(0,12)}…, got ${s.slice(0,12)}…)`);h.push({name:r.name,buf:i})}i(d,{recursive:!0,force:!0}),r(d,{recursive:!0});for(const{name:e,buf:t}of h){const n=p(d,e);r(f(n),{recursive:!0}),c(n,t)}await async function(e,t){let r;try{r=await import(u(e).href)}catch(e){throw new Error(`Downloaded site failed to import: ${e instanceof Error?e.message:String(e)}`)}const n=r.default??r.site;if(!n||"object"!=typeof n)throw new Error(`Downloaded ${e} does not default-export a site object`);const o=l(n);if(o.id!==t)throw new Error(`Catalog id "${t}" does not match the site's own id "${o.id}"`)}(p(d,n.files[0].name),n.id);const g={id:n.id,registry:e.name,repo:e.repo,commit:t.commit,version:n.version,installedAt:(new Date).toISOString()};return c(p(d,".source.json"),JSON.stringify(g,null,2)+"\n","utf8"),{dir:d,record:g}}export function listInstalled(e=process.env){const r=m(e);if(!t(r))return[];const i=[];for(const e of n(r)){const t=p(r,e,".source.json");try{s(t).isFile()&&i.push(JSON.parse(o(t,"utf8")))}catch{}}return i}export function removeInstalled(e,r=process.env){const n=p(m(r),e);return!!t(n)&&(i(n,{recursive:!0,force:!0}),!0)}
@@ -5,3 +5,5 @@ export { sites, loadSites, setSites, getSite, queryWebsite, createUniversalSite,
5
5
  export { discoverSites, extensionRoots, BUNDLED_SITES_DIR } from "./core/loader.js";
6
6
  export { createContext } from "./core/context.js";
7
7
  export type { ContextProviders, ManagedContext } from "./core/context.js";
8
+ export { DEFAULT_REGISTRY, resolveRegistries, addRegistry, removeRegistry, parseRepoSpec, loadIndex, searchRegistries, resolveEntry, installEntry, listInstalled, removeInstalled, configPath, } from "./core/registry.js";
9
+ export type { RegistrySource, RegistryFile, RegistrySiteEntry, RegistryIndex, InstalledRecord, FoundEntry, } from "./core/registry.js";
@@ -1 +1 @@
1
- import{loadEnv as e}from"./env.js";e();export{defineSite,isSite}from"./core/define-site.js";export{FormLoginStrategy}from"./capabilities/login/login-strategy.js";export{sites,loadSites,setSites,getSite,queryWebsite,createUniversalSite}from"./core/runtime.js";export{discoverSites,extensionRoots,BUNDLED_SITES_DIR}from"./core/loader.js";export{createContext}from"./core/context.js";
1
+ import{loadEnv as e}from"./env.js";e();export{defineSite,isSite}from"./core/define-site.js";export{FormLoginStrategy}from"./capabilities/login/login-strategy.js";export{sites,loadSites,setSites,getSite,queryWebsite,createUniversalSite}from"./core/runtime.js";export{discoverSites,extensionRoots,BUNDLED_SITES_DIR}from"./core/loader.js";export{createContext}from"./core/context.js";export{DEFAULT_REGISTRY,resolveRegistries,addRegistry,removeRegistry,parseRepoSpec,loadIndex,searchRegistries,resolveEntry,installEntry,listInstalled,removeInstalled,configPath}from"./core/registry.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "website-api",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "CLI and library to fetch website API data",
5
5
  "main": "./dist/src/website-api.js",
6
6
  "types": "./dist/src/website-api.d.ts",