Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion docs/specs/dor-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ Public PTY env:
- `DORMOUSE_NODE` — Node runtime used by the launcher.
- `DORMOUSE_CLI_JS` — absolute path to staged `dist/dor.js`.
- `DORMOUSE_SURFACE_ID` — stable invoking Session/surface id.
- `DORMOUSE_HOST` — hosting app kind: `vscode` or `standalone`.
- `DORMOUSE_HOST_WORKSPACE` — VS Code only: what the owning window has open —
the `.code-workspace` file when one is loaded, else the first workspace
folder. Unset under the standalone app (no workspace concept) and for an
empty VS Code window.
- `DORMOUSE_CONTROL_SOCKET` and `DORMOUSE_CONTROL_TOKEN` — private control
endpoint credentials.

Expand Down Expand Up @@ -122,7 +127,7 @@ it. `dor`'s `prebuild` builds `dor-lib-common` first so its `.d.ts` exists.

`standalone/package.json` runs `pnpm stage:dor-cli` before Tauri dev/build.
Rust resolves the staged/bundled CLI paths, starts the Node sidecar with
`DORMOUSE_NODE`, `DORMOUSE_CLI_BIN`, `DORMOUSE_CLI_JS`,
`DORMOUSE_HOST`, `DORMOUSE_NODE`, `DORMOUSE_CLI_BIN`, `DORMOUSE_CLI_JS`,
`DORMOUSE_CONTROL_SOCKET`, and `DORMOUSE_CONTROL_TOKEN`, then the shared PTY
core prepends `DORMOUSE_CLI_BIN` and sets `DORMOUSE_SURFACE_ID` per PTY.

Expand Down Expand Up @@ -235,6 +240,13 @@ from `command-detail`.
- `dor agent-browser` / `dor ab` — delegates to the user's `agent-browser`,
rendered in a Dormouse-native surface; the `ab-screencast` renderer of the
unified `browser` surface, see [dor-browser.md](dor-browser.md)
- `dor identify` — JSON identity dump: caller surface (matched locally against
`DORMOUSE_SURFACE_ID`, `null` when not visible), focused surface, and the
hosting app (`DORMOUSE_HOST` / `DORMOUSE_HOST_WORKSPACE` / runtime paths).
Composes over `surface.list` — no dedicated control method. Deliberately does
not expose the control socket: the CLI is the public API and the socket is
private plumbing.
[impl](../../dor/src/commands/identify.ts) [docs](../../dor/test/snapshots/help/identify.md)
- `dor list-panes` [impl](../../dor/src/commands/list-panes.ts) [docs](../../dor/test/snapshots/help/list-panes.md)
- `dor list-pane-surfaces` [impl](../../dor/src/commands/list-pane-surfaces.ts) [docs](../../dor/test/snapshots/help/list-pane-surfaces.md)

Expand Down
3 changes: 3 additions & 0 deletions dor/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '@stricli/core';
import { agentBrowserCommand, runAgentBrowserCli } from './commands/agent-browser.js';
import { ensureCommand } from './commands/ensure.js';
import { identifyCommand } from './commands/identify.js';
import { iframeCommand } from './commands/iframe.js';
import { killCommand } from './commands/kill.js';
import { listPaneSurfacesCommand } from './commands/list-pane-surfaces.js';
Expand Down Expand Up @@ -70,6 +71,7 @@ const COMMANDS = [
killCommand,
iframeCommand,
agentBrowserCommand,
identifyCommand,
listPanesCommand,
listPaneSurfacesCommand,
] as const satisfies readonly Command[];
Expand All @@ -83,6 +85,7 @@ const ROUTES = {
kill: killCommand.command,
iframe: iframeCommand.command,
'agent-browser': agentBrowserCommand.command,
identify: identifyCommand.command,
'list-panes': listPanesCommand.command,
'list-pane-surfaces': listPaneSurfacesCommand.command,
};
Expand Down
114 changes: 114 additions & 0 deletions dor/src/commands/identify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/** Identity dump: caller/focused surfaces plus the hosting app (kind, workspace, runtime paths). */

import { buildCommand } from '@stricli/core';
import type {
Command,
DorCommandContext,
IdFormat,
ListSurfacesResponse,
Surface,
} from './types.js';
import {
errorMessage,
parseIdFormat,
renderJson,
requireControlClient,
wantsIds,
wantsRefs,
writeStdout,
} from './shared.js';

interface IdentifyFlags {
readonly idFormat?: IdFormat;
}

export const identifyCommand: Command = {
name: 'identify',
command: buildCommand<IdentifyFlags, [], DorCommandContext>({
docs: {
brief: 'Print JSON identifying this terminal within Dormouse.',
customUsage: ['[--id-format refs|uuids|both]'],
fullDescription: `Prints a JSON object describing where this dor invocation sits within Dormouse: the caller surface (resolved from DORMOUSE_SURFACE_ID), the focused surface, and the hosting app.

host is "vscode" or "standalone". host_workspace is what the owning VS Code window has open — the .code-workspace file when one is loaded, else the root workspace folder; it is always null under the standalone app.

caller is null when no visible surface matches the invoking terminal (e.g. it was minimized to a Door); focused is null when no surface is focused. Environment-derived fields are null when the corresponding variable is absent.

Output is always JSON:
{
"caller": {
"is_browser_surface": false,
"pane_ref": "pane:1",
"surface_ref": "surface:1",
"surface_type": "terminal",
"window_ref": "window:1",
"workspace_ref": "workspace:1"
},
"cli_js_path": "/path/to/dor-cli/dist/dor.js",
"focused": { ... },
"host": "vscode",
"host_workspace": "/path/to/project",
"node_path": "/path/to/node"
}`,
},
parameters: {
flags: {
idFormat: {
kind: 'parsed',
parse: parseIdFormat,
brief: 'Handle format for surface handles.',
optional: true,
placeholder: 'refs|uuids|both',
},
},
},
func(flags) {
return runIdentifyCommand(flags, this);
},
}),
};

async function runIdentifyCommand(
flags: IdentifyFlags,
context: DorCommandContext,
): Promise<void | Error> {
const client = requireControlClient(context.options);
if (client instanceof Error) return client;

try {
const response = await client.listSurfaces({});
const env = context.options.env ?? {};
const idFormat = flags.idFormat ?? 'refs';
const caller = response.surfaces.find((surface) => surface.id === env.DORMOUSE_SURFACE_ID) ?? null;
const focused = response.surfaces.find((surface) => surface.focused) ?? null;

const payload = {
caller: renderIdentitySurface(caller, response, idFormat),
cli_js_path: env.DORMOUSE_CLI_JS ?? null,
focused: renderIdentitySurface(focused, response, idFormat),
host: env.DORMOUSE_HOST ?? null,
host_workspace: env.DORMOUSE_HOST_WORKSPACE ?? null,
node_path: env.DORMOUSE_NODE ?? null,
};
writeStdout(context, renderJson(payload));
return undefined;
} catch (error) {
return new Error(errorMessage(error));
}
}

function renderIdentitySurface(
surface: Surface | null,
response: ListSurfacesResponse,
idFormat: IdFormat,
): Record<string, unknown> | null {
if (!surface) return null;
return {
is_browser_surface: surface.type !== 'terminal',
...(wantsRefs(idFormat) ? { pane_ref: surface.paneRef } : {}),
...(wantsIds(idFormat) ? { surface_id: surface.id } : {}),
...(wantsRefs(idFormat) ? { surface_ref: surface.ref } : {}),
surface_type: surface.type,
...(wantsRefs(idFormat) ? { window_ref: response.windowRef, workspace_ref: response.workspaceRef } : {}),
};
}
38 changes: 38 additions & 0 deletions dor/test/cli-output.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,44 @@ test('agent-browser resolves the binary on PATH to an absolute binaryPath', asyn
}
});

const identifyEnv = {
DORMOUSE_SURFACE_ID: '22222222-2222-4222-8222-222222222222',
DORMOUSE_CLI_JS: '/opt/dormouse/dor-cli/dist/dor.js',
DORMOUSE_NODE: '/opt/dormouse/node',
DORMOUSE_HOST: 'vscode',
DORMOUSE_HOST_WORKSPACE: '/Users/me/projects/site',
// The control socket is private host plumbing (the CLI is the public API),
// so identify must not echo it — the snapshot proves the field is absent.
DORMOUSE_CONTROL_SOCKET: '/tmp/dormouse-control.sock',
};

test('identify json output', async () => {
const client = fixtureClient();
const result = await runCli(['identify'], { client, env: identifyEnv });
assert.deepEqual(client.requests, [{}]);
await snapshot('identify', result);
});

test('identify id-format both output', async () => {
await snapshot(
'identify-id-format-both',
await runCli(['identify', '--id-format', 'both'], { client: fixtureClient(), env: identifyEnv }),
);
});

test('identify without env reports null caller and paths', async () => {
await snapshot('identify-no-env', await runCli(['identify'], { client: fixtureClient() }));
});

test('identify reports a null caller when the calling surface is not visible', async () => {
const result = await runCli(['identify'], {
client: fixtureClient(),
env: { ...identifyEnv, DORMOUSE_SURFACE_ID: '99999999-9999-4999-8999-999999999999' },
});
assert.equal(result.exitCode, 0);
assert.equal(JSON.parse(result.stdout).caller, null);
});

test('list-panes text output', async () => {
await snapshot('list-panes-text', await runCli(['list-panes'], { client: fixtureClient() }));
});
Expand Down
2 changes: 2 additions & 0 deletions dor/test/snapshots/help/dor.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ USAGE
dor kill --surface id|ref|index [--confirm-if-read text|--confirm-dangerously]
dor iframe [--json] [--minimize] [--surface id|ref|index] <url>
dor agent-browser [--key name|--session name] [args...]
dor identify [--id-format refs|uuids|both]
dor list-panes [--id-format refs|uuids|both] [--json]
dor list-pane-surfaces [--id-format refs|uuids|both] [--json] [--pane id|ref|index]
dor --help
Expand All @@ -31,6 +32,7 @@ COMMANDS
kill Kill a terminal surface.
iframe Open a URL in an iframe surface.
agent-browser Drive a browser surface via your agent-browser install (alias: dor ab).
identify Print JSON identifying this terminal within Dormouse.
list-panes List visible panes.
list-pane-surfaces List surfaces in a pane.

Expand Down
38 changes: 38 additions & 0 deletions dor/test/snapshots/help/identify.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# dor identify

Invocation: `dor identify --help`

```text
USAGE
dor identify [--id-format refs|uuids|both]
dor identify --help

Prints a JSON object describing where this dor invocation sits within Dormouse: the caller surface (resolved from DORMOUSE_SURFACE_ID), the focused surface, and the hosting app.

host is "vscode" or "standalone". host_workspace is what the owning VS Code window has open — the .code-workspace file when one is loaded, else the root workspace folder; it is always null under the standalone app.

caller is null when no visible surface matches the invoking terminal (e.g. it was minimized to a Door); focused is null when no surface is focused. Environment-derived fields are null when the corresponding variable is absent.

Output is always JSON:
{
"caller": {
"is_browser_surface": false,
"pane_ref": "pane:1",
"surface_ref": "surface:1",
"surface_type": "terminal",
"window_ref": "window:1",
"workspace_ref": "workspace:1"
},
"cli_js_path": "/path/to/dor-cli/dist/dor.js",
"focused": { ... },
"host": "vscode",
"host_workspace": "/path/to/project",
"node_path": "/path/to/node"
}

FLAGS
[--id-format] Handle format for surface handles.
-h --help Print help information and exit
-- All subsequent inputs should be interpreted as arguments

```
28 changes: 28 additions & 0 deletions dor/test/snapshots/identify-id-format-both.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
exitCode: 0
stdout:
{
"caller": {
"is_browser_surface": false,
"pane_ref": "pane:2",
"surface_id": "22222222-2222-4222-8222-222222222222",
"surface_ref": "surface:2",
"surface_type": "terminal",
"window_ref": "window:1",
"workspace_ref": "workspace:1"
},
"cli_js_path": "/opt/dormouse/dor-cli/dist/dor.js",
"focused": {
"is_browser_surface": false,
"pane_ref": "pane:1",
"surface_id": "11111111-1111-4111-8111-111111111111",
"surface_ref": "surface:1",
"surface_type": "terminal",
"window_ref": "window:1",
"workspace_ref": "workspace:1"
},
"host": "vscode",
"host_workspace": "/Users/me/projects/site",
"node_path": "/opt/dormouse/node"
}

stderr:
19 changes: 19 additions & 0 deletions dor/test/snapshots/identify-no-env.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
exitCode: 0
stdout:
{
"caller": null,
"cli_js_path": null,
"focused": {
"is_browser_surface": false,
"pane_ref": "pane:1",
"surface_ref": "surface:1",
"surface_type": "terminal",
"window_ref": "window:1",
"workspace_ref": "workspace:1"
},
"host": null,
"host_workspace": null,
"node_path": null
}

stderr:
26 changes: 26 additions & 0 deletions dor/test/snapshots/identify.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
exitCode: 0
stdout:
{
"caller": {
"is_browser_surface": false,
"pane_ref": "pane:2",
"surface_ref": "surface:2",
"surface_type": "terminal",
"window_ref": "window:1",
"workspace_ref": "workspace:1"
},
"cli_js_path": "/opt/dormouse/dor-cli/dist/dor.js",
"focused": {
"is_browser_surface": false,
"pane_ref": "pane:1",
"surface_ref": "surface:1",
"surface_type": "terminal",
"window_ref": "window:1",
"workspace_ref": "workspace:1"
},
"host": "vscode",
"host_workspace": "/Users/me/projects/site",
"node_path": "/opt/dormouse/node"
}

stderr:
7 changes: 5 additions & 2 deletions scripts/spec-lint.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
*/
import { readFileSync, readdirSync, existsSync } from 'node:fs';
import { join, dirname, normalize } from 'node:path';
import { fileURLToPath } from 'node:url';

const ROOT = new URL('..', import.meta.url).pathname;
const ROOT = fileURLToPath(new URL('..', import.meta.url));
const SPECS_DIR = 'docs/specs';

const TOP_LEVEL_DIRS = [
Expand All @@ -37,7 +38,9 @@ const SKIP_PATH_PREFIXES = [

const specFiles = readdirSync(join(ROOT, SPECS_DIR))
.filter((f) => f.endsWith('.md'))
.map((f) => join(SPECS_DIR, f));
// Keep repo-relative paths in POSIX form on every platform — they are
// compared against forward-slash paths written in the markdown.
.map((f) => `${SPECS_DIR}/${f}`);
const allFiles = ['AGENTS.md', ...specFiles];
const problems = [];

Expand Down
1 change: 1 addition & 0 deletions standalone/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,7 @@ fn start_sidecar(app: &AppHandle) -> Result<SidecarState, String> {

let mut wrap = CommandWrap::with_new(&node_path, |c| {
c.arg(&sidecar_path)
.env("DORMOUSE_HOST", "standalone")
.env("DORMOUSE_NODE", &node_path)
.env("DORMOUSE_CLI_BIN", &dor_cli_paths.bin_dir)
.env("DORMOUSE_CLI_JS", &dor_cli_paths.entrypoint)
Expand Down
Loading
Loading