/** * One-command deployment for a standalone bot. `dsh plugin`, profiles, and unit * files are host or operating-system vocabulary, and someone who only wants a * chat bot should not have to learn any of it: `start` provisions a dedicated * profile, hands it to the platform supervisor, and streams the first run to the * terminal so the QR code is scanned where a person is actually looking. Once * the scan lands, the command returns and the bot keeps running. * * Backgrounding from the first moment — rather than running in the foreground * and migrating later — is what makes that one command. There is no second step * to forget, and the process the operator scanned into is the same one that * survives the terminal closing. Where no supervisor exists (Windows, a Linux * without systemd), `start` degrades to a foreground run instead of a dead end. * * The profile stays an ordinary profile rather than a hidden invention. `dsh * --profile ` keeps working on it, and everything the host documents about * composition, settings, and credentials still applies. * * Unit files carry no model key — that always resolves inside the host through * `ctx.credentials` — and no Lark secret either, with one exception: an operator * who supplies `LARK_APP_ID`/`LARK_APP_SECRET` through the environment gets them * copied into the unit, because a supervisor starts the process with no shell * environment and the shipped patch reads exactly those variables. Units are * written user-only (0600) for that reason. * @module dsh-lark-channel/provision */ import { ownVersion } from './version.ts'; /** Profile created when the operator names none. */ export declare const DEFAULT_PROFILE = "lark"; /** Reverse-DNS label shared by the launchd job and the systemd unit. */ export declare const SERVICE_LABEL = "dev.omdsh.dsh-lark"; /** Composition row id this plugin owns, and its section name in the settings document. */ export declare const ROW_ID = "lark-channel"; /** How long `start` watches for a scan before leaving the operator to it. */ export declare const ONBOARDING_WATCH_MS: number; /** Log size past which `start` truncates it, since no supervisor rotates it. */ export declare const LOG_TRUNCATE_BYTES: number; /** What the operator asked for, after argument parsing. */ export type Command = { readonly kind: 'start'; readonly profile: string; readonly workspace: string; } | { readonly kind: 'add' | 'remove'; readonly profile?: string; readonly name: string; } | { readonly kind: 'upgrade'; readonly profile?: string; readonly workspace?: string; } | { readonly kind: 'stop' | 'restart' | 'status'; } | { readonly kind: 'logs'; readonly follow: boolean; } | { readonly kind: 'help'; }; /** App credentials the operator supplied through the environment. */ export interface EnvCredentials { readonly appId: string; readonly appSecret: string; } /** Everything a unit file needs, resolved once so both writers agree. */ export interface ServiceSpec { /** Absolute path to `dsh`: a supervisor inherits no PATH worth trusting. */ readonly dsh: string; /** Profile to boot. */ readonly profile: string; /** Directory the host treats as the default workspace root. */ readonly workspace: string; /** `$DSH_HOME` when the operator set one, so the service reads the same home. */ readonly dshHome?: string | undefined; /** Environment-supplied app credentials, forwarded so the service sees them too. */ readonly credentials?: EnvCredentials | undefined; } /** Usage text, printed for `help`. */ /** * How to spell this command back to the person running it. * * Run through `npx`, the binary lives in a throwaway cache and no bare * `dsh-lark-channel` exists on the PATH — so telling that person to run one is * telling them to run something that does not work. Told the form they * actually used, both kinds of user can copy what they read. * * The alternative — installing ourselves globally so the bare name always * works — is a machine-wide change nobody asked for, with its own permission * and version-manager failures, made in service of a shorter string. * @param scriptPath - the running script; defaults to this process's. * @returns the command prefix to print. */ export declare function invocation(scriptPath?: string): string; /** Usage text, spelled for the way this process was started. */ export declare function usage(self?: string): string; export { ownVersion }; /** * Parse argv into one command, defaulting a bare invocation to `start` so * `npx dsh-lark-channel` on its own does the useful thing. * @param argv - arguments after the node executable and the script path. * @returns the parsed command. * @throws when a verb or flag is unknown, or a flag's value is missing. */ export declare function parseArguments(argv: readonly string[]): Command; /** * Locate an executable on PATH without shelling out, so the answer is an * absolute path a supervisor can use. * @param name - executable name, without a platform extension. * @param path - PATH to search; defaults to this process's. * @returns the absolute path, or undefined when no entry holds that executable. */ export declare function whichSync(name: string, path?: string): string | undefined; /** * Which supervisor this system offers, when it offers one. Linux is only * supervised when systemctl exists: a WSL or container without systemd should * degrade to a foreground run, not fail on a missing binary. * @param platform - platform to answer for; defaults to this process's. * @returns the supervisor kind, or undefined when the system has none. */ export declare function supervisorKind(platform?: NodeJS.Platform): 'launchd' | 'systemd' | undefined; /** The harness home the supervised process will read. */ export declare function dshHome(): string; /** Where a supervised run writes its console output. */ export declare function logPath(): string; /** Absolute path of the unit file this platform uses. */ export declare function unitPath(platform?: NodeJS.Platform): string; /** * Whether the settings document already carries this plugin's section, which is * where a completed scan persists its credentials. A top-level key is enough to * decide, so no YAML parser — and no dependency on one — is needed. * @param document - contents of the settings document. * @returns true when the section is present. */ export declare function hasCredentialSection(document: string, namespace?: string): boolean; /** * App credentials from the environment, when the operator supplied a complete * pair. These must reach the unit file: the CLI seeing them proves nothing * about the supervised process, which inherits no shell environment. * @param environment - environment to read; defaults to this process's. * @returns the pair, or undefined when either half is missing or empty. */ export declare function envCredentials(environment?: NodeJS.ProcessEnv): EnvCredentials | undefined; /** * Whether a scan is still needed: credentials exist in the environment (the * unit forwards them) or a previous scan persisted them through the host * settings service. * @returns true when the bot already has credentials to connect with. */ export declare function isOnboarded(instance?: string): boolean; /** * PATH for the supervised process. A supervisor starts it with a stunted PATH, * and two things break on that: `dsh` is a `#!/usr/bin/env node` script, so a * PATH without its interpreter crash-loops the service before it prints * anything; and the agent this bot drives runs shell commands, which expect the * tools the operator has. So the interpreter's directory and `dsh`'s own lead, * and the PATH in force at install time follows — a snapshot of the environment * the operator would have started it in by hand. * @param dsh - absolute path to the `dsh` executable. * @param execPath - the interpreter running this CLI. * @param inherited - PATH to append; defaults to this process's. * @returns a PATH value for the unit file. */ export declare function servicePath(dsh: string, execPath?: string, inherited?: string): string; /** * launchd job description for one profile. * @param spec - the resolved service description. * @returns plist XML. */ export declare function launchdPlist(spec: ServiceSpec): string; /** * systemd user unit for one profile. Output goes to the same file launchd * uses, not the journal: `start` relays that file to the terminal for the * first-run QR code, and `logs` reads it, so the journal swallowing stdout * would break both. * @param spec - the resolved service description. * @returns unit file contents. */ export declare function systemdUnit(spec: ServiceSpec): string; /** * Make unapproved dependency build scripts a decided matter before pnpm runs. * * DSH creates a profile and installs its first plugin in one command. Under * pnpm 11 that first install stops at ERR_PNPM_IGNORED_BUILDS — today over * protobufjs, tomorrow over whatever a transitive dependency brings with it — * after parking a placeholder decision in the profile. So the policy is two * halves: `strictDepBuilds: false` so an unapproved script is warned about and * skipped rather than fatal, and an explicit protobufjs entry saying this one * was considered. Both are written before the first install, and a profile a * previous CLI left mid-failure is repaired the same way. An operator's own * choice, on either half, is never overwritten. * @param profile - profile whose package-manager policy should be ready. * @param home - harness home; injectable for tests. * @throws {Error} when the profile name would write outside the home. */ export declare function ensureProfileBuildPolicy(profile: string, home?: string): void; /** * Copy any log growth past `offset` to this terminal. * @param offset - byte offset the previous relay ended at. * @returns the offset the next relay should start at. */ /** What a console filter remembers between two chunks of the log. */ export interface ConsoleFilter { /** Inside a host-library log block whose remaining lines are noise. */ suppressing: boolean; /** A line the previous chunk ended mid-way through. */ partial: string; } /** A fresh filter state, for one relay session. */ export declare function newConsoleFilter(): ConsoleFilter; /** * Keep this channel's own console lines and drop the transport library's. * * A scan is watched by relaying the supervised process's log, and that log * carries the Feishu SDK's own chatter — connection notices, a paragraph about * developer-console settings — which buried the QR code it was printed to * show. The library's blocks span several lines, so suppression runs until the * next stamped line rather than to the end of the one that opened it. The log * FILE keeps everything: a dropped `[ws]` line is exactly what diagnosing a * dead connection needs. * @param chunk - newly appended log bytes, decoded. * @param state - carried between chunks; mutated. * @returns the text worth showing, possibly empty. */ export declare function filterConsole(chunk: string, state: ConsoleFilter): string; /** The profile's own patch layer, where a deployment's extra rows live. */ export declare function patchPath(profile: string): string; /** * The patch entry one extra bot needs: its own row of this plugin, named. * @param name - the instance name. * @returns the YAML entry, marker line included. */ export declare function instanceRow(name: string): string; /** * Add one bot's row to a patch layer. * * The empty layer a fresh profile ships is the literal `[]`, which cannot hold * a list item — so the first row REPLACES it and every later one appends. * @param document - the current patch layer. * @param name - the instance name. * @returns the layer with the row, or undefined when it already had it. */ export declare function withInstanceRow(document: string, name: string): string | undefined; /** * Take one bot's row back out, from its marker to the next top-level entry. * @param document - the current patch layer. * @param name - the instance name. * @returns the layer without the row, or undefined when it had no such row. */ export declare function withoutInstanceRow(document: string, name: string): string | undefined; /** * The plugin version a profile currently has installed. * @param profile - the profile to inspect. * @returns the version, or undefined when the profile has no plugin yet. */ export declare function installedPluginVersion(profile: string): string | undefined; /** What the installed unit says this bot actually runs as. */ export interface InstalledService { readonly profile: string; readonly workspace: string; } /** * Read the profile and workspace out of the unit that is actually installed. * * Every command that touches a running bot has to act on the bot that is * running, not on a default. Guessing the profile from a constant and the * workspace from the current directory is how `upgrade` run in /tmp rewrote a * `--profile web --workspace /repo` service into `lark` and `/tmp` — the unit * file already holds both, so it is the only honest source. * @param document - the unit file's contents. * @param platform - which unit format to read. * @returns what the unit says, or undefined for a field it does not carry. */ export declare function readInstalledService(document: string, platform?: NodeJS.Platform): Partial; /** * What the running bot is, for a command that must not move it. * @returns the installed profile and workspace, each absent when unreadable. */ export declare function installedService(): Partial; /** * The newest published version, or undefined when the check cannot answer. * * Deliberately toothless: a registry that is slow, offline, or unreachable * must cost this command nothing, because every caller is doing something * else — starting a bot, reading a log — and a version notice is a courtesy. * @param timeoutMs - how long to wait before giving up. * @returns the version string, or undefined. */ export declare function latestVersion(timeoutMs?: number): Promise; /** * Compare two `major.minor.patch` versions, ignoring anything after them. * @param left - the version to judge. * @param right - the version to judge it against. * @returns true when `left` is strictly newer. */ export declare function isNewer(left: string, right: string): boolean; /** * Execute one parsed command. * @param command - what the operator asked for. */ export declare function execute(command: Command): Promise; /** * Entry point: parse, execute, and turn any failure into a diagnosed nonzero exit. * @param argv - arguments after the node executable and the script path. */ export declare function main(argv: readonly string[]): Promise; //# sourceMappingURL=provision.d.ts.map