feat(host): adaptive directory-picker default via -auto chooser
Add @deepseek-ai/dsh-host-directory-picker-auto, a node-half-only chooser that samples the host situation once at boot (bind host via a new httpServer.host getter, SSH markers, platform, DISPLAY/WAYLAND_DISPLAY) and mounts the matching dual-face backend (-native or -browse) as a real Loader entry in the in-memory root tree; the effect disposer removes it. Entry-level mounting keeps the seam's one-row-swaps-both-faces invariant: the client module table discovers the mounted backend's browser half exactly as a config row's. apps/cli now composes -auto as its directory-picker row; composing a backend row directly remains the pin.
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Adaptive chooser of the directory-picker seam: resolves the host's
|
||||
* situation once at boot (bind host, SSH launch, display session) and mounts
|
||||
* the matching dual-face backend — `-native` or `-browse` — as a real Loader
|
||||
* entry in the in-memory root tree. Because the backend arrives as an
|
||||
* ordinary entry, its browser half is discovered exactly as a config-row's
|
||||
* would be, so the seam's one-row-swaps-both-faces invariant holds for the
|
||||
* resolved choice; pinning an interaction remains composing that backend row
|
||||
* directly instead of this one.
|
||||
* @module @deepseek-ai/dsh-host-directory-picker-auto
|
||||
*/
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
// Empty type imports carry the `loader` and `httpServer` Context merges for the reads below.
|
||||
import type {} from '@cordisjs/plugin-loader'
|
||||
import type {} from '@deepseek-ai/dsh-host-webserver'
|
||||
import type { DirectoryPickerBackendKind } from './resolve.ts'
|
||||
import { resolveDirectoryPickerBackend } from './resolve.ts'
|
||||
|
||||
export type { DirectoryPickerBackendKind, DirectoryPickerEnv, DirectoryPickerHostFacts } from './resolve.ts'
|
||||
export { resolveDirectoryPickerBackend } from './resolve.ts'
|
||||
|
||||
/** Cordis plugin name. */
|
||||
export const name = 'directory-picker-auto'
|
||||
/** Required services: the effective bind host (`httpServer`) and the entry tree the backend mounts into (`loader`). */
|
||||
export const inject = ['httpServer', 'loader']
|
||||
|
||||
/** Backend package per resolved kind — fixed composition vocabulary, not a tunable. */
|
||||
const BACKEND_PACKAGES: Record<DirectoryPickerBackendKind, string> = {
|
||||
native: '@deepseek-ai/dsh-host-directory-picker-native',
|
||||
browse: '@deepseek-ai/dsh-host-directory-picker-browse',
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the backend from one boot-time sample and mount it as a Loader
|
||||
* entry; the effect's disposer removes the entry, so unloading this plugin
|
||||
* unloads both faces of the mounted backend with it.
|
||||
* @param ctx - cordis context carrying the injected `httpServer` and `loader`.
|
||||
*/
|
||||
export async function apply(ctx: Context): Promise<void> {
|
||||
const backend = resolveDirectoryPickerBackend({
|
||||
bindHost: ctx.httpServer.host,
|
||||
platform: process.platform,
|
||||
env: process.env,
|
||||
})
|
||||
await ctx.effect(async () => {
|
||||
// Root-tree create: the Loader root is in-memory (write() is a no-op), so
|
||||
// the mounted row can never be persisted back into a config file.
|
||||
const id = await ctx.loader.create({ name: BACKEND_PACKAGES[backend] })
|
||||
return () => { ctx.loader.remove(id) }
|
||||
}, 'directory-picker-auto: backend entry')
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Package-owned invariant companion for the adaptive directory-picker chooser.
|
||||
* @module @deepseek-ai/dsh-host-directory-picker-auto/invariant
|
||||
*/
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-host-directory-picker-auto'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'host-directory-picker-auto-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/** No runtime invariant: the sole effect is one boot-time Loader-entry mount owned by the plugin fiber; the store is authoritative. */
|
||||
const install: InvariantInstaller = () => {}
|
||||
|
||||
/**
|
||||
* Register the adaptive directory-picker invariant companion.
|
||||
* @param ctx - Cordis context carrying the invariant service.
|
||||
* @returns the installed registration's disposer after setup succeeds.
|
||||
*/
|
||||
export const apply = (ctx: Context): Promise<() => void> =>
|
||||
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Boot-time backend resolution for the adaptive directory-picker composition:
|
||||
* one pure decision from sampled host facts to a concrete backend kind. The
|
||||
* caller samples exactly once per boot, so the mounted capability stays
|
||||
* stable for the service lifetime as the seam requires.
|
||||
* @module @deepseek-ai/dsh-host-directory-picker-auto/resolve
|
||||
*/
|
||||
|
||||
/** Concrete interaction backend the resolver chooses between. */
|
||||
export type DirectoryPickerBackendKind = 'native' | 'browse'
|
||||
|
||||
/** Environment keys the resolution reads (a `process.env` subset). */
|
||||
export type DirectoryPickerEnv = Readonly<
|
||||
Partial<Record<'SSH_CONNECTION' | 'SSH_TTY' | 'DISPLAY' | 'WAYLAND_DISPLAY', string>>
|
||||
>
|
||||
|
||||
/** Host facts the backend choice is a pure function of, sampled once at boot. */
|
||||
export interface DirectoryPickerHostFacts {
|
||||
/** Effective webserver bind host (`127.0.0.1` or `0.0.0.0`). */
|
||||
bindHost: string
|
||||
/** Host process platform. */
|
||||
platform: NodeJS.Platform
|
||||
/** Environment sample; SSH marks a remote operator, DISPLAY/WAYLAND_DISPLAY a Linux display. */
|
||||
env: DirectoryPickerEnv
|
||||
}
|
||||
|
||||
/** An env value counts only when set and non-blank (an empty export is "unset" by shell convention). */
|
||||
const present = (value: string | undefined): boolean => value !== undefined && value !== ''
|
||||
|
||||
/**
|
||||
* Resolve which backend serves this boot. `native` requires every signal that
|
||||
* the operator can see the host display: a loopback-only bind (an
|
||||
* all-interfaces bind admits remote browsers no OS chooser can reach), no SSH
|
||||
* launch (under SSH port-forwarding the chooser would open on the unattended
|
||||
* server), and a display session (assumed on darwin/win32, `DISPLAY`/
|
||||
* `WAYLAND_DISPLAY` elsewhere). Anything ambiguous resolves to `browse`,
|
||||
* which works everywhere.
|
||||
* @param facts - the sampled host facts.
|
||||
* @returns the backend kind to mount.
|
||||
*/
|
||||
export function resolveDirectoryPickerBackend(facts: DirectoryPickerHostFacts): DirectoryPickerBackendKind {
|
||||
if (facts.bindHost !== '127.0.0.1') return 'browse'
|
||||
if (present(facts.env.SSH_CONNECTION) || present(facts.env.SSH_TTY)) return 'browse'
|
||||
if (facts.platform === 'darwin' || facts.platform === 'win32') return 'native'
|
||||
return present(facts.env.DISPLAY) || present(facts.env.WAYLAND_DISPLAY) ? 'native' : 'browse'
|
||||
}
|
||||
Reference in New Issue
Block a user