/** * One-shot migration of the pre-0.2 pane settings into the `connect` profile entry. * * Before 0.2 this plugin's pane settings lived in `$DSH_HOME/settings.yaml` * under a `dsh-connect:` section, keyed by plugin *name*. The 0.2 host does not * know that section and will not import it: `SettingsForms.importLegacyDocument()` * renames the document to `settings.yaml.imported` and then calls * `update(section, values)` for every top-level section, where the only renamed * keys are the base-bundle ones (`ui-onboarding` → `ui-settings-general`, …). * `dsh-connect` reaches `update("dsh-connect", …)` — an entry that does not * exist — and is dropped with a warning. The user's channels keep working (their * config is in the profile patch) but every pane-only choice silently reverts to * its default the first time the pane is opened. This module recovers it. * * Three properties make the migration safe to run unattended: * * - **It merges, it does not replace.** `SettingsForms.replace()` resets every * live field to its *inherited* layer and then applies the supplied ones, so a * section carrying only the legacy keys would clear every pane field the * legacy document happens not to mention — `channels` included, which is how a * migration ends up disabling every adapter. The section handed to * `replace()` is therefore always complete: what is in force now, with the * legacy values layered on top. * - **It is one-shot.** A marker beside the profile entry records that the * import ran, including the benign "there was nothing to import" case, so the * legacy document is never re-applied over later user edits. Nothing is marked * unless the outcome is final: a document that is missing is marked, a document * that is *there but unreadable* is not (see `LegacySkipReason`). * - **It never blocks or breaks the load.** A missing parser, an unparseable * document or a refused write degrades to one warning line and leaves both * files untouched for a retry after the cause is fixed. * * Secrets cannot travel through it: the legacy section is projected by * `sectionOf()`, which keeps declared non-secret keys only, and a parse failure * is reported without quoting the document (a YAML error message contains the * offending source lines, and the section it would quote holds an `appSecret`). * Anything else in that file stays where it is. * @module dsh-connect/settings/legacy-import */ import { type ConnectSection } from "./namespace.js"; /** * Name of the one-shot marker written beside the profile entry. * * Namespaced and dotted rather than a plain `${patchPath}.legacy-imported` * because the profile directory is shared: every plugin's entry lives in that * one `cordis.patch.yml`, so a bare suffix would be a file every plugin with a * migration of its own would fight over — and the first one to write it would * silently cancel the others. */ export declare const LEGACY_IMPORT_MARKER = ".dsh-connect-legacy-imported"; /** The logger surface this module uses; both members are optional. */ export interface LegacyImportLogger { warn?(...args: unknown[]): void; info?(...args: unknown[]): void; } /** * Why nothing was imported. Absent when the import succeeded. * * `unreadable` is kept apart from `no-document` because only one of the two is * final. An absent document is marked done and never looked for again; a * document that is *there* and could not be read is the one case where the * user's settings may be sitting in a file we failed to open, so it must stay * unmarked and be reported. Collapsing them — which this did until 0.9.2 — meant * a single `EACCES` (or a `settings.yaml` that is accidentally a directory) * ended the migration silently, with no marker to explain it and no warning. */ export type LegacySkipReason = "already-imported" | "no-document" | "no-section" | "not-live" | "unparseable" | "unreadable" | "parser-missing" | "write-failed"; export interface LegacyImportResult { /** True only when a section was written into the profile entry. */ imported: boolean; /** The document the section came from, when one was found. */ source?: string; /** The complete section that was written — the in-force values with the legacy ones layered on. */ section?: ConnectSection; /** Why nothing was imported; a benign value means the marker was still written. */ skipped?: LegacySkipReason; } export interface LegacyImportOptions { /** Harness home holding the legacy document; `$DSH_HOME`, then `~/.dsh`. */ home?: string; /** * Writes a complete section onto the plugin's profile entry — the * `LiveConnectSection.write` the settings pane itself saves through, so a * migrated value reconciles the running adapters exactly like a pane save. * Absent when the seam is not live (no settings service, or no resolvable * entry id), in which case nothing is marked and the import waits for a host * that can take it. */ write?: ((section: ConnectSection) => Promise) | undefined; /** The section in force now, used as the base so the write cannot clear anything. */ current: ConnectSection; /** Path of this plugin's pre-0.2 JSON state file; the last-resort source. */ statePath?: string | undefined; /** * Path of this plugin's entry in the active profile — the * `profileContext.patchPath` the import writes to, and the anchor the * one-shot marker is named after (see `legacyMarkerPath`). */ entryPath?: string | undefined; /** Where to record that the import ran; overrides the derived path. */ markerPath?: string | undefined; logger?: LegacyImportLogger | undefined; } /** * Resolve the Harness home with the same precedence as the host's own * `resolveDshHome()`: an explicit path (the host hands us `profileContext.home`, * which also covers a configured non-env home), then `$DSH_HOME` — whitespace * only counts as unset, so a blank override never resolves the home to the * filesystem root — then `~/.dsh`. A `~` in either of the first two is expanded. */ export declare function resolveHarnessHome(explicit?: string): string; /** * One document to try, and how to read the pane section out of it. * * The shape is carried alongside the path rather than guessed from the contents * because the two sources really do differ, and guessing wrong fails silently: * reading the state file as a document finds no `dsh-connect` key, and * `importLegacySection` then reports "nothing to import" for a user whose only * settings are sitting in that file. */ export interface LegacyCandidate { path: string; /** * `"document"` — a `settings.yaml`-style file, a map of *plugin sections* * keyed by plugin name, so the pane's values live under `dsh-connect`. * `"section"` — this plugin's own JSON state file, whose top level already * *is* the section (`settings-service`'s fallback store persists it flat). */ shape: "document" | "section"; } /** * The documents to try, in order. * * `settings.yaml` first: if the host has not run its own legacy import yet, the * section is still there. Then `settings.yaml.imported`, which is where the host * puts the document after renaming it — the read has to tolerate both because * the host's import waits for the loader to settle and may run before or after * ours. Finally the JSON state file this plugin used as its own fallback store: * its values are unreachable once the live peer exists (`settings-service` * prefers `live.read()` whenever it is present), so a user who only ever saved * through the fallback path would otherwise lose them silently. That file is * written flat — `persist()` stringifies the config itself, with no * `dsh-connect` wrapper — so it is read as a section, not as a document. */ export declare function legacyCandidates(options: Pick): LegacyCandidate[]; /** * Where the one-shot marker goes: beside the profile entry this import writes to. * * The anchor matters more than it looks. The marker means "this *profile entry* * has already consumed the home's legacy document", so it has to move exactly * when that entry moves and not otherwise: * * - **Not the state file.** `${statePath}.legacy-imported` — what this did until * 0.9.2 — hangs off a path the user controls (`stateDir`, the * `DSH_CONNECT_STATE_DIR` override, `settingsStatePath`) and can delete. Move * the state directory, or delete one JSON file, and the next boot believes no * import ever ran, re-reads `settings.yaml.imported` (the host's rename keeps * the `dsh-connect:` section intact) and layers the legacy values back over * everything changed in the pane since. That is the one outcome the marker * exists to prevent, reachable by tidying up a directory. * - **Not the harness home.** `$DSH_HOME` is shared by every profile, so a * marker there would let a second profile skip an import it never ran. The * profile directory is per-profile; it only moves if the profile itself does, * which is the one case where re-importing is right. * * The state-file suffix survives as a fallback for a caller with no entry path * (a host too old to hand one over), where it is still better than no marker. */ export declare function legacyMarkerPath(options: Pick): string | undefined; /** * Extract the pane section from a parsed `settings.yaml`-style document: the * `dsh-connect` mapping, projected onto declared keys, or `undefined` when the * document has no such section (or carries nothing usable under it). */ export declare function sectionFromDocument(document: unknown): ConnectSection | undefined; /** * Extract the pane section from this plugin's own JSON state file, whose top * level is already the section. A `dsh-connect` wrapper is tolerated as well, * since pasting values across from the host document is a natural thing to have * done — but the flat form is the one the file is actually written in. */ export declare function sectionOfStateDocument(document: unknown): ConnectSection | undefined; /** * Import the pre-0.2 pane settings into the profile entry, once. * * Never rejects: every failure path is reported through the returned result and * (for the ones a user can act on) a single warning line. */ export declare function importLegacySection(options: LegacyImportOptions): Promise; //# sourceMappingURL=legacy-import.d.ts.map