Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ node_modules/

# Build output
dist/
dor/src/generated-version.ts
lib/dist/
*.tsbuildinfo

Expand Down
7 changes: 7 additions & 0 deletions docs/specs/dor-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ Invariants:

- Stable ids and short refs are accepted where a surface/pane target is
accepted.
- Surface targets also accept `title:<exact display title>`. If exactly one
visible surface has that title, it is selected. If multiple visible surfaces
match, the command fails and lists the matching surface refs.
- Short refs currently use cmux-style names for implemented handles:
`surface:1`, `pane:2`.
- List output defaults to refs; commands that list handles accept
Expand Down Expand Up @@ -154,5 +157,9 @@ from `command-detail`.

- `dor split` [impl](../../dor/src/commands/split.ts) [docs](../../dor/test/snapshots/help/split.md)
- `dor ensure` [impl](../../dor/src/commands/ensure.ts) [docs](../../dor/test/snapshots/help/ensure.md)
- `dor version` [impl](../../dor/src/commands/version.ts) [docs](../../dor/test/snapshots/help/version.md)
- `dor send` [impl](../../dor/src/commands/send.ts) [docs](../../dor/test/snapshots/help/send.md)
- `dor read` [impl](../../dor/src/commands/read.ts) [docs](../../dor/test/snapshots/help/read.md)
- `dor kill` [impl](../../dor/src/commands/kill.ts) [docs](../../dor/test/snapshots/help/kill.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)
1 change: 1 addition & 0 deletions dor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"dist"
],
"scripts": {
"prebuild": "node ../scripts/generate-dor-version.mjs",
"build": "tsc -p tsconfig.json && esbuild src/dor.ts --bundle --format=esm --platform=node --outfile=dist/dor.js",
"test": "pnpm run build && node --test test/*.test.mjs"
},
Expand Down
20 changes: 20 additions & 0 deletions dor/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ import {
type StricliProcess,
} from '@stricli/core';
import { ensureCommand } from './commands/ensure.js';
import { killCommand } from './commands/kill.js';
import { listPaneSurfacesCommand } from './commands/list-pane-surfaces.js';
import { listPanesCommand } from './commands/list-panes.js';
import { readCommand } from './commands/read.js';
import { sendCommand } from './commands/send.js';
import { splitCommand } from './commands/split.js';
import { versionCommand } from './commands/version.js';
import { fail } from './commands/shared.js';
import type {
CliEnv,
Expand All @@ -31,25 +35,41 @@ export type {
EnsureSurfaceRequest,
EnsureSurfaceResponse,
IdFormat,
KillSurfaceConfirmation,
KillSurfaceRequest,
KillSurfaceResponse,
ListSurfacesRequest,
ListSurfacesResponse,
ReadSurfaceRequest,
ReadSurfaceResponse,
ResolvedSplitDirection,
SendSurfaceRequest,
SendSurfaceResponse,
SplitDirection,
SplitSurfaceRequest,
SplitSurfaceResponse,
Surface,
VersionMetadata,
} from './commands/types.js';

const COMMANDS = [
splitCommand,
ensureCommand,
versionCommand,
sendCommand,
readCommand,
killCommand,
listPanesCommand,
listPaneSurfacesCommand,
] as const satisfies readonly Command[];

const ROUTES = {
split: splitCommand.command,
ensure: ensureCommand.command,
version: versionCommand.command,
send: sendCommand.command,
read: readCommand.command,
kill: killCommand.command,
'list-panes': listPanesCommand.command,
'list-pane-surfaces': listPaneSurfacesCommand.command,
};
Expand Down
104 changes: 104 additions & 0 deletions dor/src/commands/kill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/** Private `surface.kill` command wiring and confirmation validation. */

import { buildCommand } from '@stricli/core';
import type {
Command,
DorCommandContext,
KillSurfaceConfirmation,
KillSurfaceResponse,
ParseResult,
} from './types.js';
import {
resolveControlClient,
stringParser,
writeStdout,
} from './shared.js';

interface KillFlags {
readonly confirmDangerously?: boolean;
readonly confirmIfRead?: string;
readonly surface: string;
}

export const killCommand: Command = {
name: 'kill',
helpPatches: [
{
scope: 'root',
findReplace: [
' dor kill [--confirm-dangerously] [--confirm-if-read text] (--surface id|ref|index)',
' dor kill --surface id|ref|index [--confirm-if-read text|--confirm-dangerously]',
],
},
{
scope: 'command-usage',
findReplace: [
' dor kill [--confirm-dangerously] [--confirm-if-read text] (--surface id|ref|index)',
' dor kill --surface id|ref|index [--confirm-if-read text|--confirm-dangerously]',
],
},
],
command: buildCommand<KillFlags, [], DorCommandContext>({
docs: {
brief: 'Kill a terminal surface.',
fullDescription: `Kills a terminal surface. One confirmation mode is required.

--confirm-if-read kills only if dor read --surface <surface> would return visible text containing the provided text. The text must contain at least 4 non-whitespace characters.

--confirm-dangerously kills without further confirmation. Use only when automation has already validated the target.

Text output:
killed surface:3`,
},
parameters: {
flags: {
confirmDangerously: { kind: 'boolean', brief: 'Kill without further confirmation.', optional: true, withNegated: false },
confirmIfRead: { kind: 'parsed', parse: stringParser, brief: 'Kill only if dor read contains this text.', optional: true, placeholder: 'text' },
surface: { kind: 'parsed', parse: stringParser, brief: 'Surface to kill.', placeholder: 'id|ref|index' },
},
},
func: runKillCommand,
}),
};

async function runKillCommand(this: DorCommandContext, flags: KillFlags): Promise<void | Error> {
const confirmation = parseConfirmation(flags);
if (!confirmation.ok) return new Error(confirmation.message);

const clientResult = resolveControlClient(this.options);
if (!clientResult.ok) return new Error(clientResult.message);

try {
const response = await clientResult.value.killSurface({
confirmation: confirmation.value,
surface: flags.surface,
});
writeStdout(this, renderKillResponse(response));
return undefined;
} catch (error) {
return new Error(error instanceof Error ? error.message : String(error));
}
}

function parseConfirmation(flags: KillFlags): ParseResult<KillSurfaceConfirmation> {
const confirmations = [
flags.confirmDangerously === true,
flags.confirmIfRead !== undefined,
].filter(Boolean).length;

if (confirmations !== 1) {
return { ok: false, message: 'dor kill requires exactly one confirmation mode' };
}

if (flags.confirmDangerously) return { ok: true, value: { mode: 'dangerously' } };

const text = flags.confirmIfRead?.trim() ?? '';
if (text.replace(/\s/g, '').length < 4) {
return { ok: false, message: 'dor kill --confirm-if-read requires at least 4 non-whitespace characters' };
}
return { ok: true, value: { mode: 'if-read', text } };
}

function renderKillResponse(response: KillSurfaceResponse): string {
return `${response.status} ${response.surfaceRef}\n`;
}
89 changes: 89 additions & 0 deletions dor/src/commands/read.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/** Private `surface.read` command wiring and response rendering. */

import { buildCommand } from '@stricli/core';
import type {
Command,
DorCommandContext,
ReadSurfaceResponse,
} from './types.js';
import {
resolveControlClient,
stringParser,
writeStdout,
} from './shared.js';

interface ReadFlags {
readonly json?: boolean;
readonly lines?: number;
readonly scrollback?: boolean;
readonly surface?: string;
}

export const readCommand: Command = {
name: 'read',
command: buildCommand<ReadFlags, [], DorCommandContext>({
docs: {
brief: 'Read terminal text from a surface.',
fullDescription: `By default, reads the visible screen text from the target surface. Use --scrollback to include terminal history, and --lines to limit how much text is returned.

If --surface is omitted, Dormouse uses the caller surface from DORMOUSE_SURFACE_ID, then the focused surface.

Text mode prints terminal text directly.

JSON output:
{
"workspace_ref": "workspace:1",
"surface_id": "...",
"surface_ref": "surface:3",
"text": "..."
}`,
},
parameters: {
flags: {
json: { kind: 'boolean', brief: 'Print JSON output.', optional: true, withNegated: false },
lines: { kind: 'parsed', parse: parseLineCount, brief: 'Maximum number of lines to return.', optional: true, placeholder: 'count' },
scrollback: { kind: 'boolean', brief: 'Include terminal scrollback/history instead of only the visible screen.', optional: true, withNegated: false },
surface: { kind: 'parsed', parse: stringParser, brief: 'Surface to read.', optional: true, placeholder: 'id|ref|index' },
},
},
func: runReadCommand,
}),
};

async function runReadCommand(this: DorCommandContext, flags: ReadFlags): Promise<void | Error> {
const clientResult = resolveControlClient(this.options);
if (!clientResult.ok) return new Error(clientResult.message);

try {
const response = await clientResult.value.readSurface({
...(flags.lines !== undefined ? { lines: flags.lines } : {}),
scrollback: flags.scrollback === true,
surface: flags.surface,
});
writeStdout(this, renderReadResponse(response, flags.json === true));
return undefined;
} catch (error) {
return new Error(error instanceof Error ? error.message : String(error));
}
}

function parseLineCount(input: string): number {
const value = Number(input);
if (!Number.isInteger(value) || value <= 0) {
throw new SyntaxError(`invalid --lines '${input}'`);
}
return value;
}

function renderReadResponse(response: ReadSurfaceResponse, json: boolean): string {
if (json) {
return `${JSON.stringify({
workspace_ref: response.workspaceRef,
...(response.surfaceId ? { surface_id: response.surfaceId } : {}),
surface_ref: response.surfaceRef,
text: response.text,
}, null, 2)}\n`;
}

return response.text;
}
Loading
Loading