dsh-home-hosted 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +48 -0
  3. package/cordis.patch.yml +6 -0
  4. package/docs/DESIGN.md +114 -0
  5. package/lib/client.js +9 -0
  6. package/lib/index.js +3694 -0
  7. package/lib/index.js.map +7 -0
  8. package/lib/types/boot/common.d.ts +40 -0
  9. package/lib/types/boot/escape.d.ts +59 -0
  10. package/lib/types/boot/fallback.d.ts +10 -0
  11. package/lib/types/boot/index.d.ts +12 -0
  12. package/lib/types/boot/ladder.d.ts +4 -0
  13. package/lib/types/boot/launchd.d.ts +8 -0
  14. package/lib/types/boot/systemd.d.ts +10 -0
  15. package/lib/types/boot/types.d.ts +94 -0
  16. package/lib/types/boot/windows.d.ts +16 -0
  17. package/lib/types/boot/xdg.d.ts +3 -0
  18. package/lib/types/config.d.ts +15 -0
  19. package/lib/types/home-hosted/config-file.d.ts +23 -0
  20. package/lib/types/home-hosted/dsh-entry.d.ts +22 -0
  21. package/lib/types/home-hosted/entries.d.ts +20 -0
  22. package/lib/types/home-hosted/launch.d.ts +25 -0
  23. package/lib/types/home-hosted/launcher.d.ts +38 -0
  24. package/lib/types/home-hosted/panel-control.d.ts +34 -0
  25. package/lib/types/home-hosted/panel.d.ts +44 -0
  26. package/lib/types/home-hosted/resolve.d.ts +34 -0
  27. package/lib/types/home-hosted/runtime.d.ts +20 -0
  28. package/lib/types/home-hosted/token.d.ts +20 -0
  29. package/lib/types/index.d.ts +15 -0
  30. package/lib/types/rpc.d.ts +11 -0
  31. package/lib/types/service.d.ts +125 -0
  32. package/lib/types/settings.d.ts +13 -0
  33. package/lib/types/shared/contracts.d.ts +245 -0
  34. package/lib/types/tools.d.ts +14 -0
  35. package/lib/types/util/exec.d.ts +29 -0
  36. package/lib/types/util/fsx.d.ts +9 -0
  37. package/lib/types/util/paths.d.ts +12 -0
  38. package/package.json +108 -0
@@ -0,0 +1,40 @@
1
+ import type { BootState } from '../shared/contracts.js';
2
+ import type { BootActionResult, BootProviderContext } from './types.js';
3
+ export declare function errorMessage(error: unknown): string;
4
+ /** Existence check through the injected seam, so a probe is testable without touching the host. */
5
+ export declare function existsOf(ctx: BootProviderContext, file: string): boolean;
6
+ export declare function posixJoin(...parts: string[]): string;
7
+ /** `$XDG_CONFIG_HOME`, else `~/.config`. */
8
+ export declare function configHome(ctx: BootProviderContext): string;
9
+ /** `%LOCALAPPDATA%`, else `~\AppData\Local`. */
10
+ export declare function localAppData(ctx: BootProviderContext): string;
11
+ /** A writable scratch directory, used to stage a unit before `sudo install`. */
12
+ export declare function tempDir(ctx: BootProviderContext): string;
13
+ export declare function currentUser(ctx: BootProviderContext): string | null;
14
+ export declare function uidOf(ctx: BootProviderContext): string | null;
15
+ export interface FileOwnership {
16
+ exists: boolean;
17
+ owned: boolean;
18
+ text: string | null;
19
+ reason: string | null;
20
+ }
21
+ /**
22
+ * An artifact belongs to this plugin only when it carries the marker. A file
23
+ * we cannot read is treated as foreign: we must never overwrite or delete a
24
+ * unit somebody else wrote.
25
+ */
26
+ export declare function inspectOwned(file: string, marker: string): FileOwnership;
27
+ /** Write our artifact if the bytes differ; never overwrite a foreign one. */
28
+ export declare function writeOwned(file: string, marker: string, content: string, mode?: number): {
29
+ changed: boolean;
30
+ refusal: string | null;
31
+ };
32
+ /** Delete our artifact only; a missing file is already the desired state. */
33
+ export declare function removeOwned(file: string, marker: string): {
34
+ removed: boolean;
35
+ refusal: string | null;
36
+ };
37
+ export declare function failed(detail: string, extra?: Partial<BootActionResult>): BootActionResult;
38
+ /** Installed + enabled + last run failed is `enabled-failing`; never a guessed success. */
39
+ export declare function bootState(installed: boolean, enabled: boolean, failing: boolean): BootState;
40
+ export declare function isInstalledState(state: BootState): boolean;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Escaping for every format this plugin writes.
3
+ *
4
+ * None of this builds a shell command: a unit file, a plist, a `.desktop`
5
+ * entry, a task XML and a registry value each have their own parser, and each
6
+ * value is quoted for exactly that parser. Values that carry control
7
+ * characters (newlines included) are rejected outright, because a newline is
8
+ * how a value escapes its field in every one of these formats.
9
+ */
10
+ export declare function assertNoControl(value: string, what: string): string;
11
+ export declare function assertUnitName(name: string): string;
12
+ export declare function assertMarker(marker: string): string;
13
+ export declare function assertEnvKey(key: string): string;
14
+ /** POSIX-absolute, Windows-drive-absolute or UNC; checked without consulting the host OS. */
15
+ export declare function isAbsolutePath(value: string): boolean;
16
+ export declare function assertAbsolute(value: string, what: string): string;
17
+ export declare function assertLabel(value: string): string;
18
+ export declare function assertArg(value: string, what?: string): string;
19
+ /** Single-quote a value the way a POSIX shell would, for `commands[]`. */
20
+ export declare function shellQuote(value: string): string;
21
+ /** The exact line a person can paste into a shell. */
22
+ export declare function shellCommand(program: string, args: string[]): string;
23
+ /**
24
+ * A word in `ExecStart=`/`ExecStop=`. systemd expands `%` specifiers and `$`
25
+ * variables there before it splits the line, so both are pinned, and `;` — the
26
+ * multi-command separator — is quoted rather than left bare.
27
+ */
28
+ export declare function systemdExecWord(value: string): string;
29
+ /**
30
+ * A single-line free-text setting such as `Description=` (specifiers only).
31
+ */
32
+ export declare function systemdText(value: string): string;
33
+ /**
34
+ * A path setting (`WorkingDirectory=`). systemd takes these verbatim — it does
35
+ * not unquote, and does not unescape `\%` or `\$` — so the only correct
36
+ * encoding is the raw value; anything that would break the line is refused.
37
+ */
38
+ export declare function systemdPath(value: string, what?: string): string;
39
+ /** `Environment=KEY=value`, quoted as one assignment when the value needs it. */
40
+ export declare function systemdEnvLine(key: string, value: string): string;
41
+ export declare function xmlEscape(value: string): string;
42
+ export declare function xmlUnescape(value: string): string;
43
+ /** A marker inside an XML comment may not contain `--`. */
44
+ export declare function assertXmlCommentSafe(value: string): string;
45
+ export declare function desktopWord(value: string): string;
46
+ export declare function desktopExec(program: string, args: string[]): string;
47
+ /** A `Key=value` line in a `.desktop` file; the marker is written verbatim. */
48
+ export declare function desktopValue(key: string, value: string): string;
49
+ /** Quote one argv word the way `CreateProcess`/`cmd` parse a command line. */
50
+ export declare function windowsArg(value: string): string;
51
+ export declare function windowsCommandLine(program: string, args: string[]): string;
52
+ /** A line for a `.cmd` wrapper: `%` is a batch metacharacter and is doubled. */
53
+ export declare function batchCommandLine(program: string, args: string[]): string;
54
+ /** A single-quoted PowerShell literal. */
55
+ export declare function powershellLiteral(value: string): string;
56
+ /** Quote one word the way `cmd.exe` takes it, for a copy-pasteable `commands[]` entry. */
57
+ export declare function cmdQuote(value: string): string;
58
+ export declare function windowsDisplayCommand(program: string, args: string[]): string;
59
+ export declare function assertRegistryValueName(name: string): string;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The two terminals of the ladder: a container (an OS boot entry would land
3
+ * outside it, so the right answer is the container's own restart policy) and a
4
+ * platform this plugin has no mechanism for.
5
+ */
6
+ import type { BootProvider, BootProviderContext } from './types.js';
7
+ /** A human-readable description of the container we are inside, or null. */
8
+ export declare function detectContainer(ctx: BootProviderContext): string | null;
9
+ export declare function createContainerProvider(ctx: BootProviderContext): BootProvider;
10
+ export declare function createUnsupportedProvider(platform: NodeJS.Platform): BootProvider;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The boot-autostart family: one provider per OS mechanism, ordered into a
3
+ * ladder. The plugin runs inside dsh, so it can only install, verify and
4
+ * remove an OS-level entry — it never runs at boot itself.
5
+ */
6
+ export type { BootActionResult, BootLadder, BootLadderOptions, BootProvider, BootProviderContext, BootProviderStatus, BootRunOptions, BootRunResult, BootRunner, BootSpec, } from './types.js';
7
+ export { bootProviders, createBootLadder } from './ladder.js';
8
+ export { launchdLabel, launchdPlist } from './launchd.js';
9
+ export { systemdSystemUnit, systemdUserUnit } from './systemd.js';
10
+ export { xdgDesktopEntry } from './xdg.js';
11
+ export { registerTaskScript, scheduledTaskXml, unregisterTaskScript, windowsRunPayload } from './windows.js';
12
+ export { detectContainer } from './fallback.js';
@@ -0,0 +1,4 @@
1
+ import type { BootLadder, BootLadderOptions, BootProvider, BootProviderContext } from './types.js';
2
+ /** Ordered by preference within a platform; the terminal providers come last. */
3
+ export declare function bootProviders(ctx: BootProviderContext, platform?: NodeJS.Platform): BootProvider[];
4
+ export declare function createBootLadder(options?: BootLadderOptions): BootLadder;
@@ -0,0 +1,8 @@
1
+ import type { BootProvider, BootProviderContext, BootSpec } from './types.js';
2
+ /** Both scopes label with the same reverse-DNS prefix. */
3
+ export declare const LAUNCHD_LABEL_PREFIX = "dev.home-hosted.";
4
+ export declare function launchdLabel(spec: BootSpec): string;
5
+ /** The full property list. The marker rides in an XML comment, which launchd ignores. */
6
+ export declare function launchdPlist(spec: BootSpec): string;
7
+ export declare function createLaunchdAgentProvider(ctx: BootProviderContext): BootProvider;
8
+ export declare function createLaunchdDaemonProvider(ctx: BootProviderContext): BootProvider;
@@ -0,0 +1,10 @@
1
+ import type { BootProvider, BootProviderContext, BootSpec } from './types.js';
2
+ /** `Type=exec` with an explicit environment; boot capability comes from linger, not from this file. */
3
+ export declare function systemdUserUnit(spec: BootSpec): string;
4
+ /**
5
+ * The system unit. `StartLimit*` belongs in `[Unit]`, not `[Service]`; `User=`
6
+ * is written only when we are root and know the target account.
7
+ */
8
+ export declare function systemdSystemUnit(spec: BootSpec, user?: string | null): string;
9
+ export declare function createSystemdUserProvider(ctx: BootProviderContext): BootProvider;
10
+ export declare function createSystemdSystemProvider(ctx: BootProviderContext): BootProvider;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The boot ladder's frozen surface.
3
+ *
4
+ * A provider is stateless: everything it needs arrives in `BootSpec` and a
5
+ * context of injectable seams. Nothing here consults the operating system at
6
+ * module scope, so importing this family is safe on every platform.
7
+ */
8
+ import type { BootCandidate, BootMechanism, BootState, BootStatus } from '../shared/contracts.js';
9
+ export interface BootSpec {
10
+ /** Absolute program (node), never a shim. */
11
+ command: string;
12
+ /** Absolute CLI entry plus args. */
13
+ args: string[];
14
+ /** Absolute working directory. */
15
+ cwd: string;
16
+ /** Absolute values only (PATH, HOME, HHOSTED_HOME, …). */
17
+ env: Record<string, string>;
18
+ /** Text identifying this plugin as the writer of an artifact. */
19
+ marker: string;
20
+ /** e.g. 'home-hosted'. */
21
+ unitName: string;
22
+ /** Human label. */
23
+ label: string;
24
+ /** Absolute directory the entry may log into. */
25
+ logDir: string;
26
+ }
27
+ export interface BootActionResult {
28
+ ok: boolean;
29
+ changed: boolean;
30
+ detail: string;
31
+ /** Exact commands a person can run when privilege is missing. */
32
+ commands: string[];
33
+ needsPrivilege: boolean;
34
+ }
35
+ export interface BootProvider {
36
+ mechanism: BootMechanism;
37
+ detect(): Promise<BootCandidate>;
38
+ status(spec: BootSpec): Promise<{
39
+ state: BootState;
40
+ unitPath: string | null;
41
+ detail: string;
42
+ commands: string[];
43
+ }>;
44
+ install(spec: BootSpec): Promise<BootActionResult>;
45
+ uninstall(spec: BootSpec): Promise<BootActionResult>;
46
+ }
47
+ /** One injected process result; `run()` from `util/exec.ts` is structurally assignable. */
48
+ export interface BootRunResult {
49
+ code: number | null;
50
+ stdout: string;
51
+ stderr: string;
52
+ error?: string | null;
53
+ }
54
+ export interface BootRunOptions {
55
+ cwd?: string;
56
+ env?: Record<string, string | undefined>;
57
+ timeoutMs?: number;
58
+ }
59
+ /** argv-only, never a shell. `run` from `util/exec.ts` satisfies this. */
60
+ export type BootRunner = (command: string, args: string[], options?: BootRunOptions) => Promise<BootRunResult>;
61
+ export interface BootProviderContext {
62
+ platform: NodeJS.Platform;
63
+ home: string;
64
+ env: Record<string, string | undefined>;
65
+ run: BootRunner;
66
+ sudo: () => Promise<boolean>;
67
+ isRoot: boolean;
68
+ /** Existence probe; defaults to `fs.existsSync`. Injected so container/PID-1 detection is testable. */
69
+ exists?: (file: string) => boolean;
70
+ }
71
+ export interface BootLadderOptions {
72
+ platform?: NodeJS.Platform;
73
+ home?: string;
74
+ env?: Record<string, string | undefined>;
75
+ run?: BootRunner;
76
+ sudo?: () => Promise<boolean>;
77
+ exists?: (file: string) => boolean;
78
+ }
79
+ export interface BootLadder {
80
+ providers: BootProvider[];
81
+ detect(): Promise<{
82
+ platform: BootStatus['platform'];
83
+ candidates: BootCandidate[];
84
+ }>;
85
+ status(spec: BootSpec, mechanism?: BootMechanism): Promise<BootStatus>;
86
+ install(spec: BootSpec, mechanism?: BootMechanism): Promise<BootActionResult & {
87
+ mechanism: BootMechanism | null;
88
+ status: BootStatus;
89
+ }>;
90
+ uninstall(spec: BootSpec, mechanism?: BootMechanism): Promise<BootActionResult & {
91
+ status: BootStatus;
92
+ }>;
93
+ }
94
+ export type BootProviderStatus = Awaited<ReturnType<BootProvider['status']>>;
@@ -0,0 +1,16 @@
1
+ import type { BootProvider, BootProviderContext, BootSpec } from './types.js';
2
+ export declare const RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
3
+ export declare const MARKER_KEY = "HKCU\\Software\\home-hosted";
4
+ export interface WindowsRunPayload {
5
+ /** The exact `Run` value data. */
6
+ data: string;
7
+ /** Set when the command line exceeded the `Run` value limit and a `.cmd` wrapper is needed. */
8
+ wrapperPath: string | null;
9
+ wrapperContent: string | null;
10
+ }
11
+ export declare function windowsRunPayload(ctx: BootProviderContext, spec: BootSpec): WindowsRunPayload;
12
+ export declare function scheduledTaskXml(spec: BootSpec): string;
13
+ export declare function registerTaskScript(spec: BootSpec): string;
14
+ export declare function unregisterTaskScript(name: string): string;
15
+ export declare function createWindowsTaskProvider(ctx: BootProviderContext): BootProvider;
16
+ export declare function createWindowsRunProvider(ctx: BootProviderContext): BootProvider;
@@ -0,0 +1,3 @@
1
+ import type { BootProvider, BootProviderContext, BootSpec } from './types.js';
2
+ export declare function xdgDesktopEntry(spec: BootSpec): string;
3
+ export declare function createXdgAutostartProvider(ctx: BootProviderContext): BootProvider;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The Cordis row configuration: operator overrides only. Everything a person
3
+ * changes in the plugin's own page lives in `settings.json` under the plugin
4
+ * state directory, so the two never fight over the same field.
5
+ */
6
+ import z from '@deepseek-ai/schemastery';
7
+ export interface Config {
8
+ /** Override the plugin state directory (defaults to `$DSH_HOME/dsh-home-hosted`). */
9
+ stateDir?: string;
10
+ /** Override the home-hosted executable used in generated boot entries. */
11
+ homeHostedCommand?: string;
12
+ /** The entry this plugin manages for the running harness. */
13
+ defaultEntryId: string;
14
+ }
15
+ export declare const Config: z<Config>;
@@ -0,0 +1,23 @@
1
+ import type { ServerEntry, ServerEntryPatch } from '../shared/contracts.js';
2
+ export interface RawConfig {
3
+ meta?: Record<string, unknown>;
4
+ servers?: ServerEntry[];
5
+ [key: string]: unknown;
6
+ }
7
+ export interface ConfigReadResult {
8
+ raw: RawConfig | null;
9
+ exists: boolean;
10
+ error: string | null;
11
+ }
12
+ export declare function readConfig(home: string): ConfigReadResult;
13
+ /**
14
+ * Write the file back with home-hosted's own `meta` contract: a config records
15
+ * the release that wrote it and the shape it wrote, so an unstamped file still
16
+ * reads as the current schema.
17
+ */
18
+ export declare function writeConfig(home: string, raw: RawConfig, writtenBy?: string): void;
19
+ export declare function findEntry(raw: RawConfig, id: string): ServerEntry | null;
20
+ /** Insert or replace one entry, preserving every other key in the file. */
21
+ export declare function upsertEntry(raw: RawConfig, entry: ServerEntry): RawConfig;
22
+ export declare function patchEntry(raw: RawConfig, id: string, patch: ServerEntryPatch): RawConfig;
23
+ export declare function removeEntry(raw: RawConfig, id: string): RawConfig;
@@ -0,0 +1,22 @@
1
+ import type { ServerEntry } from '../shared/contracts.js';
2
+ import type { CliLaunch } from './launch.js';
3
+ /**
4
+ * Which profile this process is running.
5
+ *
6
+ * `DSH_PROFILE` alone is not trustworthy: it can be inherited from a parent
7
+ * shell that ran a different profile (that is how an entry once ended up booting
8
+ * `web` under another profile's home). The process's own argv is authoritative,
9
+ * then the profile directory the launcher pointed at, then the env var.
10
+ */
11
+ export declare function detectProfile(argv: readonly string[], env?: Record<string, string | undefined>): string;
12
+ export interface DshFacts {
13
+ id: string;
14
+ port: number | null;
15
+ host: string;
16
+ profile: string | null;
17
+ dshHome: string;
18
+ launch: CliLaunch | null;
19
+ }
20
+ /** Where the running harness's own CLI entry is, if we can tell. */
21
+ export declare function resolveDshLaunch(): Promise<CliLaunch | null>;
22
+ export declare function buildDshEntry(facts: DshFacts): ServerEntry;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * What this plugin decides about a server entry, and how that intent maps onto
3
+ * the entry: only the keys in `OWNED_ENTRY_KEYS` are ever written, so a person's
4
+ * own command, args, env, health block and everything else survive untouched.
5
+ */
6
+ import type { EntryIntent, OnPortConflict, ServerEntry, ServerEntryPatch } from '../shared/contracts.js';
7
+ export declare const DEFAULT_ON_PORT_CONFLICT: OnPortConflict;
8
+ export declare function defaultIntent(id: string): EntryIntent;
9
+ /** The owned keys as a patch, preserving any sibling keys inside `stop`. */
10
+ export declare function ownedPatch(intent: EntryIntent, live?: ServerEntry | null): ServerEntryPatch;
11
+ /** Owned keys whose live value differs from the intent. */
12
+ export declare function ownedDrift(live: ServerEntry | null, intent: EntryIntent): string[];
13
+ /** Only the owned keys, as they were before this plugin touched the entry. */
14
+ export declare function snapshotOwned(live: ServerEntry): ServerEntry;
15
+ /**
16
+ * Restore an entry to what it was before adoption. A key the snapshot does not
17
+ * carry was inherited from the panel's defaults, and a PATCH cannot delete a
18
+ * key, so it is restored to that schema default instead.
19
+ */
20
+ export declare function restorePatch(live: ServerEntry, snapshot: ServerEntry | null): ServerEntryPatch;
@@ -0,0 +1,25 @@
1
+ import type { BootSpec } from '../boot/types.js';
2
+ export interface CliLaunch {
3
+ program: string;
4
+ /** Args that must precede the command, e.g. the CLI entry path. */
5
+ args: string[];
6
+ cliEntry: string | null;
7
+ shimPath: string | null;
8
+ source: 'override' | 'entry' | 'shim-target' | 'shim';
9
+ }
10
+ export declare function which(command: string): Promise<string | null>;
11
+ /** Turn a resolved CLI path into an absolute program + argv. */
12
+ export declare function resolveShimmedCli(absolute: string, nodePath?: string): CliLaunch | null;
13
+ export declare function resolveHomeHostedLaunch(override?: string | null): Promise<CliLaunch | null>;
14
+ export interface HomeHostedFacts {
15
+ home: string;
16
+ projectDir?: string | null;
17
+ version?: string | null;
18
+ }
19
+ /** A deterministic environment for a boot-time entry, with absolute values. */
20
+ export declare function homeHostedEnv(facts: HomeHostedFacts, launch: CliLaunch, extra?: Record<string, string>): Record<string, string>;
21
+ export interface BootSpecOptions {
22
+ stateDir: string;
23
+ marker?: string;
24
+ }
25
+ export declare function buildHomeHostedBootSpec(facts: HomeHostedFacts, launch: CliLaunch, options: BootSpecOptions): BootSpec;
@@ -0,0 +1,38 @@
1
+ export interface LauncherOptions {
2
+ stateDir: string;
3
+ dshHome: string;
4
+ /** Where the pinned copy is right now; recorded as the fast path. */
5
+ resolvedEntry: string | null;
6
+ /** The pinned copy's version, recorded for diagnostics. */
7
+ resolvedVersion?: string | null;
8
+ /** The plugin's own package root, as a search hint. */
9
+ pluginRoot?: string | null;
10
+ minVersion: string;
11
+ marker?: string;
12
+ }
13
+ export interface LauncherWrite {
14
+ path: string;
15
+ changed: boolean;
16
+ }
17
+ interface LauncherRecord {
18
+ entry: string | null;
19
+ version: string | null;
20
+ pluginRoot: string | null;
21
+ writtenAt: number;
22
+ }
23
+ export declare function launcherDir(stateDir: string): string;
24
+ export declare function launcherPath(stateDir: string): string;
25
+ export declare function launcherRecordPath(stateDir: string): string;
26
+ export declare function readLauncherRecord(stateDir: string): LauncherRecord | null;
27
+ /** The generated script's source; exported so a test can inspect it. */
28
+ export declare function buildLauncherSource(options: LauncherOptions): string;
29
+ export declare function writeLauncher(options: LauncherOptions): LauncherWrite;
30
+ /** Run the launcher the way the boot entry will, and read the version it answers. */
31
+ export declare function preflightLauncher(stateDir: string, timeoutMs?: number): Promise<string | null>;
32
+ /** The version a CLI entry belongs to, read from the nearest package.json. */
33
+ export declare function versionOfEntry(entry: string): string | null;
34
+ /** Candidate entries under one root: a flat install, or a pnpm store. */
35
+ export declare function candidatesUnder(root: string): string[];
36
+ /** Highest satisfying candidate among explicit roots; exported for tests. */
37
+ export declare function findCandidate(roots: string[], minVersion: string): string | null;
38
+ export {};
@@ -0,0 +1,34 @@
1
+ import type { PanelControlResult } from '../shared/contracts.js';
2
+ import { run } from '../util/exec.js';
3
+ import type { CliLaunch } from './launch.js';
4
+ export interface PanelControlDeps {
5
+ /** The CLI to run, already resolved. */
6
+ launch: CliLaunch | null;
7
+ home: string;
8
+ projectDir?: string | null;
9
+ stateDir: string;
10
+ /** Environment for the spawned CLI (absolute PATH, HOME, HHOSTED_HOME). */
11
+ env: Record<string, string>;
12
+ timeoutMs?: number;
13
+ }
14
+ /** Start the preferred CLI as a detached panel; the CLI itself does the detaching. */
15
+ export declare function startPanel(deps: PanelControlDeps): Promise<PanelControlResult>;
16
+ export declare function takeoverHelperPath(stateDir: string): string;
17
+ export declare function takeoverLogPath(stateDir: string): string;
18
+ /** The generated helper's source; exported so a test can inspect it. */
19
+ export declare function buildTakeoverSource(deps: PanelControlDeps, oldPid: number | null, marker?: string): string;
20
+ export declare function writeTakeoverHelper(deps: PanelControlDeps, oldPid: number | null): string;
21
+ /**
22
+ * Spawn the helper detached and return immediately: the old panel's shutdown is
23
+ * about to stop this very process, so waiting for the result would lose it.
24
+ */
25
+ export declare function spawnTakeover(deps: PanelControlDeps, oldPid: number | null): PanelControlResult;
26
+ /** Install the pinned range globally, so the `global` preference has something to use. */
27
+ export declare function installGlobal(range: string, options?: {
28
+ pnpm?: boolean;
29
+ run?: typeof run;
30
+ }): Promise<{
31
+ ok: boolean;
32
+ detail: string;
33
+ output: string;
34
+ }>;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The home-hosted HTTP API client.
3
+ *
4
+ * Every call carries the plugin's own API token; the panel verifies it from its
5
+ * secrets file on each request. A write through this client is the safe path:
6
+ * the config store records the bytes it wrote, so the panel's own file watcher
7
+ * never treats the change as an external edit.
8
+ */
9
+ import type { ServerEntry, ServerEntryPatch, ServerEntryView } from '../shared/contracts.js';
10
+ export declare class PanelError extends Error {
11
+ readonly code: string;
12
+ readonly status: number | null;
13
+ /** The panel's own diagnostic, when it sent one. */
14
+ readonly detail: unknown;
15
+ constructor(message: string, code: string, status?: number | null,
16
+ /** The panel's own diagnostic, when it sent one. */
17
+ detail?: unknown);
18
+ }
19
+ export interface PanelClientOptions {
20
+ baseUrl: string;
21
+ token: string;
22
+ timeoutMs?: number;
23
+ }
24
+ export declare class PanelClient {
25
+ private readonly baseUrl;
26
+ private readonly token;
27
+ private readonly timeoutMs;
28
+ constructor(options: PanelClientOptions);
29
+ private request;
30
+ listServers(): Promise<ServerEntryView[]>;
31
+ getServer(id: string): Promise<ServerEntryView>;
32
+ createServer(entry: ServerEntry): Promise<ServerEntryView>;
33
+ updateServer(id: string, patch: ServerEntryPatch): Promise<ServerEntryView>;
34
+ deleteServer(id: string): Promise<void>;
35
+ startServer(id: string): Promise<void>;
36
+ stopServer(id: string): Promise<void>;
37
+ restartServer(id: string): Promise<void>;
38
+ /** Re-list the port's listeners and stop what is not the panel's own tree. */
39
+ freePort(id: string): Promise<{
40
+ stopped?: number[];
41
+ }>;
42
+ }
43
+ /** Prove a token works, without needing any write permission. */
44
+ export declare function verifyToken(baseUrl: string, token: string, timeoutMs?: number): Promise<boolean>;
@@ -0,0 +1,34 @@
1
+ import type { CliStatus } from '../shared/contracts.js';
2
+ import type { CliLaunch } from './launch.js';
3
+ /** The range the plugin ships and is tested against. */
4
+ export declare const EXPECTED_RANGE = "^0.6.1";
5
+ /** Oldest release whose API and config schema this plugin relies on. */
6
+ export declare const MIN_SUPPORTED_VERSION = "0.4.1";
7
+ /** Oldest release whose config schema accepts `onPortConflict: kill`. */
8
+ export declare const MIN_KILL_VERSION = "0.6.0";
9
+ export type CliPreference = 'pinned' | 'global';
10
+ export interface CliResolution {
11
+ launch: CliLaunch | null;
12
+ status: CliStatus;
13
+ }
14
+ export interface ResolveCliOptions {
15
+ override?: string | null;
16
+ /** Which copy to run when both exist. */
17
+ prefer?: CliPreference;
18
+ /** Test seam: the pinned package's manifest path, or null when it is not installed. */
19
+ packageManifest?: () => string | null;
20
+ /** Test seam: ask a CLI for its own version. */
21
+ readVersion?: (launch: CliLaunch) => Promise<string | null>;
22
+ /** Test seam: locate a command on PATH. */
23
+ findOnPath?: (command: string) => Promise<string | null>;
24
+ timeoutMs?: number;
25
+ }
26
+ /** Where the pinned `home-hosted` manifest is, resolved from this module. */
27
+ export declare function pinnedManifestPath(): string | null;
28
+ /** The `home-hosted` bin entry a package manifest declares. */
29
+ export declare function binEntryFromManifest(manifestPath: string): string | null;
30
+ /** The first `x.y.z[-pre]` in a CLI's output. */
31
+ export declare function parseVersion(output: string | null): string | null;
32
+ /** Numeric comparison; a prerelease sorts below its own release. */
33
+ export declare function compareVersions(a: string, b: string): number;
34
+ export declare function resolveCli(options?: ResolveCliOptions): Promise<CliResolution>;
@@ -0,0 +1,20 @@
1
+ export interface PanelRuntime {
2
+ version: string;
3
+ pid: number;
4
+ url: string;
5
+ probeUrl?: string;
6
+ protocol?: string;
7
+ port: number;
8
+ bindHost?: string;
9
+ projectDir?: string;
10
+ dataRoot?: string;
11
+ configPath?: string;
12
+ logFile?: string;
13
+ }
14
+ export declare function readRuntime(home: string): PanelRuntime | null;
15
+ export declare function pidAlive(pid: number): boolean;
16
+ /**
17
+ * Ask the panel's public health route whether it is answering. Uses the raw
18
+ * http/https modules so a self-signed TLS pair answers too.
19
+ */
20
+ export declare function probePanel(url: string, timeoutMs?: number): Promise<boolean>;
@@ -0,0 +1,20 @@
1
+ import type { RunResult } from '../util/exec.js';
2
+ export type CliExecutor = (args: string[], env: Record<string, string | undefined>) => Promise<RunResult>;
3
+ export interface EnsureTokenOptions {
4
+ home: string;
5
+ stateDir: string;
6
+ exec: CliExecutor;
7
+ }
8
+ export interface EnsureTokenResult {
9
+ token: string | null;
10
+ /** True when this call created the token. */
11
+ enrolled: boolean;
12
+ detail: string;
13
+ }
14
+ export declare function storedTokenPath(stateDir: string): string;
15
+ export declare function readStoredToken(stateDir: string): string | null;
16
+ export declare function storeToken(stateDir: string, token: string): void;
17
+ export declare function generateToken(): string;
18
+ /** Whether home-hosted already holds an API token (only its hash is on disk). */
19
+ export declare function apiTokenEnrolled(home: string): boolean;
20
+ export declare function ensureToken(options: EnsureTokenOptions): Promise<EnsureTokenResult>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `dsh-home-hosted` — boot autostart for the home-hosted panel, and home-hosted
3
+ * server management, from inside DeepSeek Harness.
4
+ *
5
+ * The plugin supplies no server entries of its own: everything it manages is
6
+ * declared by the person using the page, and nothing here knows a blessed id
7
+ * beyond the one it is told to treat as the running harness.
8
+ */
9
+ import type { Context } from '@deepseek-ai/cordis';
10
+ import { Config } from './config.js';
11
+ import type { Config as ConfigShape } from './config.js';
12
+ export declare const name = "dsh-home-hosted";
13
+ export { Config };
14
+ export declare const inject: string[];
15
+ export declare function apply(ctx: Context, config?: ConfigShape): void;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The browser half's one entry point.
3
+ *
4
+ * The route is registered on DeepSeek Harness's shared `/api` channel, so the
5
+ * physical carrier has already applied the Host/Origin fence and browser
6
+ * authentication before this handler runs: only the local operator reaches it.
7
+ * Domain failures travel inside the envelope, protocol failures as HTTP status.
8
+ */
9
+ import type { Context } from '@deepseek-ai/cordis';
10
+ import type { HomeHostedService } from './service.js';
11
+ export declare function registerRpc(ctx: Context, service: HomeHostedService): void;