{"_id":"@adhd/sox-source-provider","_rev":"2-181895c1f2c07a10c0915326e3709642","name":"@adhd/sox-source-provider","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@adhd/sox-source-provider","version":"0.1.0","license":"MIT","_id":"@adhd/sox-source-provider@0.1.0","maintainers":[{"name":"pseudosky","email":"skywinston.sk@gmail.com"}],"sox":{"area":"data","group":"source","concerns":["no-clone file tree enumeration + content read (GitHub, Bitbucket, local filesystem)","SourceRef URL normalization + validation (github.com/owner/repo[@ref], bitbucket.org/ws/repo[@ref], local:/abs/path)","ProviderRegistry — scheme-routed provider resolution, consumers never instanceof a concrete provider","createFakeProvider — first-class in-memory test double, same interface as real providers","typed SourceProviderError taxonomy (auth, rate-limit, truncation, not-found, transient)"],"invariants":["no search()/discovery — explicit refs only (D-1)","SourceRef.parse() THROWS InvalidSourceRefError on unparseable input — never returns a partial ref","Manifest.truncated === true means the entry list is incomplete — consumers MUST NOT infer absence from a truncated manifest","every thrown error is a typed subclass of SourceProviderError","no on-disk or in-memory caching — SourceProvider is the I/O layer only"],"entrypoints":["dist/index.js"]},"dist":{"shasum":"caedf92c5f56e5f4e5b881b98a7ea8ed8bac623c","tarball":"https://registry.npmjs.org/@adhd/sox-source-provider/-/sox-source-provider-0.1.0.tgz","fileCount":50,"integrity":"sha512-KN0AM2LTgkqu6+wu/uhFIGMNnaJVqaojinWKIsYA/Ei3fIp9pEpKQO5y1mvz1xUB/wFpd93ZaB2YNDbLtpzSOg==","signatures":[{"sig":"MEQCIA7CbpLmAhFTDSvpdvQyB2zVhVBh+AmaFHFyGJfESuCsAiAr6IgFnRldefmYe+2wulVcVjDeQmvIKignmUYWkai5FA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":133963},"main":"./dist/index.js","type":"module","_from":"file:adhd-sox-source-provider-0.1.0.tgz","types":"./dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"private":false,"_npmUser":{"name":"pseudosky","email":"skywinston.sk@gmail.com"},"_resolved":"/private/var/folders/yg/cfczgtx54bzfh74lx2_mv0z80000gp/T/4ee26629187cf38c0357862493f0303e/adhd-sox-source-provider-0.1.0.tgz","_integrity":"sha512-KN0AM2LTgkqu6+wu/uhFIGMNnaJVqaojinWKIsYA/Ei3fIp9pEpKQO5y1mvz1xUB/wFpd93ZaB2YNDbLtpzSOg==","_npmVersion":"11.6.2","description":"Unified SCM/filesystem abstraction — file tree enumeration and raw content retrieval from GitHub, Bitbucket, and the local filesystem through a single SourceProvider interface, without cloning. Provider registry, SourceRef URL normalization, and a first-c","directories":{},"_nodeVersion":"24.11.1","dependencies":{"ignore":"^7.0.5","undici":"^6.21.0","fast-glob":"^3.3.3"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/sox-source-provider_0.1.0_1784239337605_0.9543146695653069","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@adhd/sox-source-provider","version":"0.1.1","description":"Unified SCM/filesystem abstraction — file tree enumeration and raw content retrieval from GitHub, Bitbucket, and the local filesystem through a single SourceProvider interface, without cloning. Provider registry, SourceRef URL normalization, and a first-c","license":"MIT","private":false,"publishConfig":{"access":"public"},"engines":{"node":">=18"},"type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"dependencies":{"fast-glob":"^3.3.3","ignore":"^7.0.5","undici":"^6.21.0"},"sox":{"area":"data","group":"source","concerns":["no-clone file tree enumeration + content read (GitHub, Bitbucket, local filesystem)","SourceRef URL normalization + validation (github.com/owner/repo[@ref], bitbucket.org/ws/repo[@ref], local:/abs/path)","ProviderRegistry — scheme-routed provider resolution, consumers never instanceof a concrete provider","createFakeProvider — first-class in-memory test double, same interface as real providers","typed SourceProviderError taxonomy (auth, rate-limit, truncation, not-found, transient)"],"invariants":["no search()/discovery — explicit refs only (D-1)","SourceRef.parse() THROWS InvalidSourceRefError on unparseable input — never returns a partial ref","Manifest.truncated === true means the entry list is incomplete — consumers MUST NOT infer absence from a truncated manifest","every thrown error is a typed subclass of SourceProviderError","no on-disk or in-memory caching — SourceProvider is the I/O layer only"],"entrypoints":["dist/index.js"]},"keywords":["github","bitbucket","filesystem","provider","typescript"],"repository":{"type":"git","url":"git+https://github.com/PseudoSky/adhd.git"},"homepage":"https://github.com/PseudoSky/adhd","_id":"@adhd/sox-source-provider@0.1.1","bugs":{"url":"https://github.com/PseudoSky/adhd/issues"},"_integrity":"sha512-kRnYBSKy8u12+fZT0C47IzXa4ybWf0r6pCV0hJEFtV8yNaLDf7VrZuH2vUAUIhrhcJfUgpHnLJdZSrQ3S+7Hrw==","_resolved":"/private/var/folders/yg/cfczgtx54bzfh74lx2_mv0z80000gp/T/b9806303daf6c63e994baccbe27facce/adhd-sox-source-provider-0.1.1.tgz","_from":"file:adhd-sox-source-provider-0.1.1.tgz","_nodeVersion":"24.11.1","_npmVersion":"11.6.2","dist":{"integrity":"sha512-kRnYBSKy8u12+fZT0C47IzXa4ybWf0r6pCV0hJEFtV8yNaLDf7VrZuH2vUAUIhrhcJfUgpHnLJdZSrQ3S+7Hrw==","shasum":"2330ae1895beede42312d8ee844e715059a69680","tarball":"https://registry.npmjs.org/@adhd/sox-source-provider/-/sox-source-provider-0.1.1.tgz","fileCount":52,"unpackedSize":148103,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIDIqvwAbk4WxHa0CMYbUv4XDCVd+DycgbroLfKXNSPYWAiEAmHEA5Vs1HG7jxDeBG8kvY0wSeD2EHOTgGv1nS62/ciI="}]},"_npmUser":{"name":"pseudosky","email":"skywinston.sk@gmail.com"},"directories":{},"maintainers":[{"name":"pseudosky","email":"skywinston.sk@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/sox-source-provider_0.1.1_1788566487199_0.19507204242700937"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-16T22:02:17.471Z","modified":"2026-09-05T00:01:27.591Z","0.1.0":"2026-07-16T22:02:17.735Z","0.1.1":"2026-09-05T00:01:27.385Z"},"license":"MIT","description":"Unified SCM/filesystem abstraction — file tree enumeration and raw content retrieval from GitHub, Bitbucket, and the local filesystem through a single SourceProvider interface, without cloning. Provider registry, SourceRef URL normalization, and a first-c","maintainers":[{"name":"pseudosky","email":"skywinston.sk@gmail.com"}],"readme":"# @adhd/sox-source-provider\n\nA single `SourceProvider` interface for enumerating file trees and reading raw file content from GitHub, Bitbucket, or the local filesystem — **without cloning**. Consumers write code against the interface and a `ProviderRegistry` once; which backend actually answers is resolved from the URL scheme of a `SourceRef`, so nothing in calling code ever does `instanceof GitHubProvider`. A first-class in-memory fake ships alongside the real providers for testing.\n\n```bash\npnpm add @adhd/sox-source-provider\n```\n\n## Quick start\n\nThis example uses the local filesystem provider — no tokens or network access required — against a real directory:\n\n```typescript\nimport { createLocalProvider, SourceRef } from '@adhd/sox-source-provider';\n\nconst provider = createLocalProvider(); // scans the OS filesystem directly\nconst ref = SourceRef.parse('/abs/path/to/my-project');\n\nconst manifest = await provider.fileTree(ref);\nconsole.log(manifest.truncated); // false\nconsole.log(manifest.entries.map((e) => e.path));\n// [ 'README.md', 'src', 'src/index.ts', ... ]\n\nconst readme = await provider.content(ref, 'README.md');\nconsole.log(readme); // file contents as a UTF-8 string, or null if missing\n```\n\n## Referencing a source: `SourceRef`\n\n`SourceRef.parse()` is the one entry point for turning a raw string into a validated, immutable reference. It throws `InvalidSourceRefError` rather than ever returning a partial ref.\n\n```typescript\nimport { SourceRef } from '@adhd/sox-source-provider';\n\nSourceRef.parse('github:owner/repo').toString();               // \"github.com/owner/repo\"\nSourceRef.parse('github:owner/repo@v1.2.3').toString();         // \"github.com/owner/repo@v1.2.3\"\nSourceRef.parse('https://github.com/owner/repo/tree/main').toString(); // \"github.com/owner/repo@main\"\nSourceRef.parse('bitbucket:my-workspace/my-repo@develop').toString(); // \"bitbucket.org/my-workspace/my-repo@develop\"\nSourceRef.parse('/abs/path/to/repo').toString();                // \"local:/abs/path/to/repo\"\nSourceRef.parse('~/projects/my-app').toString();                // \"local:/home/you/projects/my-app\"\n```\n\n```typescript\ninterface SourceRef {\n  readonly scheme: string;    // \"github\" | \"bitbucket\" | \"local\"\n  readonly authority: string; // \"github.com\" | \"bitbucket.org\" | \"\" (local)\n  readonly path: string;      // \"owner/repo\" | \"workspace/repo\" | \"/path/to/repo\"\n  readonly ref?: string;      // \"main\" | \"v1.2.3\" | \"abc1234\" | \"refs/heads/feature\" | undefined\n  toString(): string;\n}\n```\n\nA `SourceRef` never encodes a file subpath — even a GitHub blob URL pointing at a specific file (`.../blob/main/README.md`) has its `ref` extracted, but the trailing file path is dropped. The file path is always a separate argument to `provider.content(ref, path)`.\n\n## Resolving providers: `ProviderRegistry`\n\nFor code that needs to work across schemes, register each provider once and resolve by `SourceRef`:\n\n```typescript\nimport {\n  DefaultProviderRegistry,\n  createGitHubProvider,\n  createBitbucketProvider,\n  createLocalProvider,\n  SourceRef,\n} from '@adhd/sox-source-provider';\n\nconst registry = new DefaultProviderRegistry();\nregistry.register('github.com', () => createGitHubProvider({ token: process.env.GITHUB_TOKEN! }));\nregistry.register('bitbucket.org', () => createBitbucketProvider({ token: process.env.BITBUCKET_TOKEN! }));\nregistry.register('local', () => createLocalProvider());\n\nasync function readFile(rawRef: string, path: string): Promise<string | null> {\n  const ref = SourceRef.parse(rawRef);\n  const provider = registry.getProvider(ref); // resolved by ref.authority, or \"local\"\n  return provider.content(ref, path);\n}\n\nawait readFile('github:owner/repo', 'package.json');\nawait readFile('/abs/path/to/repo', 'package.json');\n```\n\n```typescript\ninterface ProviderRegistry {\n  register(scheme: string, factory: () => SourceProvider, opts?: { force?: boolean }): void;\n  registerMany(schemes: string[], factory: () => SourceProvider): void;\n  getProvider(ref: SourceRef): SourceProvider;      // throws ProviderNotFoundError if unregistered\n  registeredSchemes(): string[];\n  deregister(scheme: string): boolean;\n  createProvider(scheme: string): SourceProvider;   // fresh instance, bypassing the per-scheme cache\n}\n```\n\n`getProvider()` lazily creates one cached instance per scheme on first use (so a provider isn't re-authenticated or re-connected on every call). Call `deregister(scheme)` then `register(scheme, factory)` again to force a fresh instance, e.g. after rotating a token.\n\n## The `SourceProvider` interface\n\nEvery concrete provider — GitHub, Bitbucket, local, and the fake — implements exactly this shape:\n\n```typescript\ninterface SourceProvider {\n  supportedSchemes(): string[];\n\n  fileTree(ref: SourceRef, path?: string): Promise<Manifest>;\n  // path scopes the walk to a subdirectory; entries[].path is still relative\n  // to the tree root, never to `path`.\n  // THROWS ProviderAuthenticationError, ProviderRateLimitError, ManifestTooLargeError, FileNotFoundError\n\n  content(ref: SourceRef, path: string): Promise<string | null>;\n  // returns null if the path doesn't exist (or, for binary content, if UTF-8 decoding fails)\n  // THROWS ProviderAuthenticationError, ProviderRateLimitError\n\n  contentStream?(ref: SourceRef, path: string): Promise<ReadableStream<Uint8Array> | null>; // optional\n  getRevision?(ref: SourceRef): Promise<string>;                                            // optional\n\n  isAvailable(): boolean; // local validation only, no network calls\n}\n```\n\n```typescript\ninterface Manifest {\n  revision: string;           // commit SHA (SCM) or filesystem snapshot hash (local)\n  defaultBranch?: string;     // present for SCM providers\n  rootUri: SourceRef;\n  truncated: boolean;         // true = entries is incomplete; never infer absence from a truncated manifest\n  entries: FileEntry[];\n  metadata: ManifestMetadata;\n  fetchedAt: string;          // ISO timestamp\n}\n\ninterface FileEntry {\n  path: string;         // relative to Manifest.rootUri, POSIX separators\n  type: 'file' | 'dir' | 'symlink';\n  size: number;         // bytes; 0 for directories\n  sha: string;          // git SHA-1 for SCM providers, SHA-256 hex for local\n  contentUrl?: string;  // present for SCM providers\n  mode?: string;        // e.g. \"100644\", \"100755\"\n  language?: string;    // populated only if the provider computes it\n  lastModified?: string; // present for local, absent for SCM\n}\n\ninterface ManifestMetadata {\n  hashAlgorithm: 'sha1' | 'sha256';\n  entryCount: number;   // may exceed entries.length when truncated\n  totalSize?: number;\n  treeSha?: string;     // present for GitHub — unchanged treeSha means content hasn't changed\n}\n```\n\n## Providers\n\n### GitHub\n\n```typescript\nimport { createGitHubProvider } from '@adhd/sox-source-provider';\n\nconst provider = createGitHubProvider({\n  token: process.env.GITHUB_TOKEN!,   // Personal Access Token, classic or fine-grained (Contents permission)\n  // baseUrl: 'https://ghes.example.com/api/v3', // GitHub Enterprise Server\n  // maxTreeEntries: 100_000,                    // default; ManifestTooLargeError above this\n  // retry: { maxRetries: 3, baseDelayMs: 1000, maxDelayMs: 60_000 },\n});\n```\n\nUses the Git Trees API (`?recursive=1`) for the common case. If the tree exceeds `maxTreeEntries` and no `path` was given, it throws `ManifestTooLargeError` — retry with an explicit subdirectory `path`, which falls back to the Contents API (not subject to the Trees API's 100K-entry ceiling). Rate limits and auth failures surface as `ProviderRateLimitError` / `ProviderAuthenticationError` with the real `Retry-After`/reset headers threaded through.\n\n### Bitbucket\n\n```typescript\nimport { createBitbucketProvider } from '@adhd/sox-source-provider';\n\nconst provider = createBitbucketProvider({\n  token: process.env.BITBUCKET_TOKEN!, // App Password or OAuth token, repository read access\n  // baseUrl: 'https://bitbucket.example.com/2.0', // Data Center/Server\n  // maxEntriesPerPage: 100, // Bitbucket's API hard limit\n  // maxPages: 100,          // truncates after 10,000 entries\n});\n```\n\nBitbucket has no single recursive-tree endpoint, so this provider walks the Source API's per-directory listings depth-first, following the real `values`/`next` pagination shape.\n\n### Local filesystem\n\n```typescript\nimport { createLocalProvider } from '@adhd/sox-source-provider';\n\nconst provider = createLocalProvider({\n  // allowedBasePath: '/abs/allowed/root', // refs outside this base throw\n  // respectGitignore: true,               // default; honors every .gitignore found in the tree\n  // ignorePatterns: ['*.env'],            // merged with .gitignore rules\n  // maxContentSize: 104_857_600,          // default 100 MB; content() over this returns null\n  // hashAlgorithm: 'sha256',              // default; 'sha1' also supported\n  // includePatterns: ['**/*.ts'],         // glob allow-list, applied after ignores\n});\n```\n\nEnumerates the real OS filesystem with `fast-glob`, respects nested `.gitignore` files (each one scoped to its own subdirectory), always excludes `.git/`, and hashes file content directly (`sha256` by default) rather than relying on any SCM's blob SHA. `fileTree(ref, path)` scoping and path-traversal are both enforced — a `content()` call that walks outside the scanned root throws `FileNotFoundError`, not a partial read.\n\n### Fake provider (testing)\n\n`createFakeProvider` is a first-class export, not a test-only import — it implements the identical `SourceProvider` interface, so integration tests exercise real consumer code with no `instanceof` branching:\n\n```typescript\nimport { createFakeProvider, SourceRef } from '@adhd/sox-source-provider';\n\nconst ref = SourceRef.parse('github.com/owner/repo@main');\nconst provider = createFakeProvider({\n  trees: {\n    'github.com/owner/repo@main': [\n      { path: 'README.md', content: '# My Project' },\n      { path: 'src/index.ts', content: 'export const x = 42' },\n    ],\n  },\n  // allowMissingFiles: false,      // default; content() for an unlisted path throws FileNotFoundError\n  // truncateAfter: 500,            // simulate Manifest.truncated once entries exceed this\n  // simulateErrors: {\n  //   fileTree: { errorClass: ProviderRateLimitError, afterCalls: 2, maxThrows: 1 },\n  // },\n});\n\nconst manifest = await provider.fileTree(ref);\nconst readme = await provider.content(ref, 'README.md'); // \"# My Project\"\n```\n\n`sha` and `size` for each `FakeProviderEntry` are auto-computed from `content` when omitted (git-blob SHA-1 for SCM-scheme trees, SHA-256 for local-scheme trees) — the fake produces the same shape of manifest a real provider would.\n\n## Error taxonomy\n\nEvery error thrown by this package subclasses `SourceProviderError`:\n\n```typescript\nclass SourceProviderError extends Error {}\n\nclass ProviderNotFoundError extends SourceProviderError { scheme: string; }\nclass ProviderAuthenticationError extends SourceProviderError { scheme: string; }\nclass ProviderRateLimitError extends SourceProviderError { scheme: string; retryAfterMs: number; resetAt?: string; }\nclass SchemeAlreadyRegisteredError extends SourceProviderError { scheme: string; }\nclass InvalidSourceRefError extends SourceProviderError { raw: string; }\nclass FileNotFoundError extends SourceProviderError { ref: string; path: string; }\nclass ManifestTooLargeError extends SourceProviderError { ref: string; maxEntries: number; estimatedTotal: number; }\nclass ProviderTransientError extends SourceProviderError { scheme: string; retryAfterMs?: number; } // network timeouts, 5xx — caller should retry\n```\n\n```typescript\nimport { InvalidSourceRefError, SourceProviderError } from '@adhd/sox-source-provider';\n\ntry {\n  SourceRef.parse('not a valid ref');\n} catch (err) {\n  if (err instanceof InvalidSourceRefError) {\n    console.error(`bad ref \"${err.raw}\"`);\n  }\n}\n```\n\n## Invariants\n\n- **No `search()` / discovery.** Every operation takes an explicit `SourceRef` — there is no repo-search or listing-by-query surface.\n- **`SourceRef.parse()` never returns a partial ref.** Unparseable input always throws `InvalidSourceRefError`.\n- **`Manifest.truncated === true` means the entry list is incomplete.** Never infer completeness (or absence of a file) from a truncated manifest — re-fetch scoped to a subdirectory instead.\n- **Every thrown error is a typed `SourceProviderError` subclass** — safe to `instanceof`-narrow across every provider.\n- **No caching in this package.** `SourceProvider` is I/O only; caching manifests or content across calls is the consumer's responsibility.\n","readmeFilename":"README.md","homepage":"https://github.com/PseudoSky/adhd","keywords":["github","bitbucket","filesystem","provider","typescript"],"repository":{"type":"git","url":"git+https://github.com/PseudoSky/adhd.git"},"bugs":{"url":"https://github.com/PseudoSky/adhd/issues"}}