/** * Pure logic for `tdcv2 pack`: parsing the registry index, verifying a download * by hash, keeping the store's books, and registering the store in the config. * * The network and the unzip live in `pack.ts`; keeping the decisions here — what * the index means, whether a download is intact, which paths a bundle owns — * makes them testable without touching the wire. * * Bundles are axis-pure — one language (`en`), or one country (`usa`), or * `common` — because language and country are independent dimensions that * compose (US English = common + en + usa). They all unpack into ONE tree: * * /.tdcv2-installed.json * /common/… * /en/… * /countries/usa/… * * The address path is the only level that has to exist, so it is the only level * there is. The store is a single scan root, registered in `dataPaths` once, and * a bundle is no longer identifiable by a folder named after it — which is what * the bookkeeping file is for: it records the paths each bundle owns, so * `pack remove` deletes exactly those and `pack list` knows what is installed. */ /** The folder inside a bundle's zip that holds its pack tree. */ export declare const BUNDLE_PACKS_DIR = "packs"; /** * The store's bookkeeping file. * * A DOTFILE because it sits at the root of a scan root, and the pack loader * skips ignored NAMES rather than unknown extensions — a plain `installed.json` * here would be read as a pack at address `installed`. */ export declare const INSTALLED_FILE = ".tdcv2-installed.json"; /** Bumped only if the record's shape changes in a way older tools misread. */ export declare const INSTALLED_SCHEMA_VERSION = 1; export declare class PackError extends Error { readonly name = "PackError"; } /** One downloadable bundle, as described by the registry `index.json`. */ export interface PackBundle { readonly id: string; readonly name: string; readonly description: string; /** Path of the zip relative to the registry base URL, e.g. `bundles/en.zip`. */ readonly file: string; readonly bytes: number; readonly sha256: string; /** * What the registry calls this revision of the bundle. Optional: today's * index declares none, and the digest already tells two revisions apart — * but the store writes down whatever the registry did say, so a registry * that starts versioning its bundles is understood without a client change. */ readonly version: string | undefined; readonly locale: string | undefined; readonly country: string | undefined; /** Human-readable list of what the bundle provides (informational). */ readonly contents: readonly string[]; /** * Continents a country belongs to, and roughly where it sits as [longitude, latitude]. * * Both come from the registry so the interactive picker can group and plot a country without * keeping a copy of world geography — the picker exists in three languages, and three copies * would be three copies that drift. Absent for languages and for `common`, and absent from an * older index, which is why nothing here is required. */ readonly regions: readonly string[] | undefined; readonly point: readonly [number, number] | undefined; } /** The registry catalogue. */ export interface PackIndex { readonly schemaVersion: number; readonly description: string | undefined; readonly bundles: readonly PackBundle[]; } /** Parse and validate a registry `index.json`. Malformed input is a clear error. */ export declare function parseIndex(text: string): PackIndex; /** Find a bundle by id, or throw a helpful error listing what is available. */ export declare function findBundle(index: PackIndex, id: string): PackBundle; /** Verify downloaded bytes against the expected sha256 (case-insensitive hex). */ export declare function verifySha256(data: Uint8Array, expected: string): boolean; /** One bundle as the store has it. */ export interface InstalledBundle { readonly id: string; /** * The paths this bundle owns, relative to the store and always `/`-separated. * `pack remove` deletes exactly these; a bundle in a shared tree cannot be * found by looking for a folder with its name. */ readonly paths: readonly string[]; /** What the registry called this revision, or `""` when it called it nothing. */ readonly version: string; /** Digest of the archive it came from; `""` for an install migrated from the old layout. */ readonly sha256: string; /** How many files it wrote. */ readonly files: number; } /** The bookkeeping file's contents. */ export interface InstalledRecord { readonly schemaVersion: number; /** Sorted by id, so the file is stable to diff. */ readonly bundles: readonly InstalledBundle[]; } /** Where the bookkeeping file lives. */ export declare function installedFilePath(store: string): string; /** * Read the store's books. A missing file means an empty store; a malformed one * is an ERROR rather than an empty store, because "nothing is installed" would * make `pack remove` claim there is nothing to delete while the files sit there. */ export declare function readInstalled(store: string): InstalledRecord; /** Write the store's books, ids sorted so re-installing the same set is a no-diff. */ export declare function writeInstalled(store: string, record: InstalledRecord): void; /** The record with `entry` put in place of any earlier install of the same id. */ export declare function withBundle(record: InstalledRecord, entry: InstalledBundle): InstalledRecord; /** The record without `id`. */ export declare function withoutBundle(record: InstalledRecord, id: string): InstalledRecord; /** * Bundle ids the store has, from its books. Missing store → nothing installed * (not an error). A tree somebody unpacked by hand is not "installed": nothing * records what it owns, so nothing could remove it cleanly either. */ export declare function installedBundleIds(store: string): string[]; /** * The paths a bundle owns, given every file it carries (store-relative, * `/`-separated). * * Usually one path: every file of `ru` sits under `ru/`, every file of `russia` * under `countries/russia/`, so the longest shared directory IS the subtree the * bundle owns — and `countries/`, which several bundles share, is correctly not * claimed by any of them. A bundle whose files diverge at the top level owns * each of those top-level entries instead, which keeps the answer an antichain: * no owned path contains another. */ export declare function bundleOwnedPaths(files: readonly string[]): string[]; /** True if `child` is the same as, or nested under, `parent`. Guards zip-slip. */ export declare function isPathInside(child: string, parent: string): boolean; /** * Add a scan root to a config file's `dataPaths`. Written relative when it sits * under the config's folder (the way `init` writes paths), de-duplicated by * resolved path, and written back preserving every other setting — including the * ones this tool knows nothing about. Returns whether anything changed. * * `pack add` calls this with the STORE, once, however many bundles land in it. */ export declare function addDataPath(configPath: string, path: string): { added: boolean; stored: string; }; /** * The inverse of {@link addDataPath}: drop a scan root from `dataPaths` (matched * by resolved path, so a relative or absolute entry both go). Returns whether * anything was removed. The bundled default for those addresses, being a * lower-priority root, simply resurfaces. */ export declare function removeDataPath(configPath: string, path: string): { removed: boolean; }; /** * Drop every `dataPaths` entry that points INSIDE the store (the store itself is * kept). That is the per-bundle registration the old layout needed — a hundred * of them for a hundred bundles — and after the move each one names a folder that * no longer exists. Returns how many went. */ export declare function removeDataPathsInside(configPath: string, store: string): number; /** * Every file under `dir`, as `/`-separated paths relative to it, sorted. * Dotfiles included: `_locale.json` and anything else the packs carry has to * travel with them, and deciding what is "really" data is the loader's job. */ export declare function walkRelative(dir: string, prefix?: string): string[]; /** Delete `dir` and every empty parent up to (but never including) `stopAt`. */ export declare function pruneEmptyDirs(dir: string, stopAt: string): void; /** One bundle's tree, moved out of its old folder. */ export interface StoreMove { readonly id: string; /** Store-relative source, e.g. `ru/packs/ru`. */ readonly from: string; /** Store-relative destination, e.g. `ru`. */ readonly to: readonly string[]; readonly files: number; } /** What {@link migrateStore} did, for the caller to report. */ export interface StoreMigration { readonly store: string; readonly moves: readonly StoreMove[]; /** * Files that were in a bundle's old folder but OUTSIDE its `packs/` tree. * They belong to no pack address, so they are left exactly where they are * rather than being guessed at or deleted. */ readonly leftovers: readonly string[]; readonly droppedDataPaths: number; /** The store, as written into `dataPaths`; undefined if it was already there. */ readonly registered: string | undefined; } /** Bundle folders in the old layout: `//packs/…`. */ export declare function legacyBundleIds(store: string): string[]; /** * Move a store written by an older tdcv2 into the flat layout, in place. * * Returns undefined when there is nothing to move. Everything is PLANNED before * anything moves: a store half in one layout and half in the other is worse than * one that refused and said why, and the refusal costs the user only the two * minutes it takes to move the offending file themselves. * * Anyone who ran `pack add` before the flat store has `//packs/…` on * disk and one `dataPaths` entry per bundle in their config; both are fixed here, * so the first `tdcv2 pack` after an upgrade is all it takes. */ export declare function migrateStore(store: string, configPath: string): StoreMigration | undefined; //# sourceMappingURL=pack-core.d.ts.map