@fonlan/dsh-quick-commands 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.
- package/LICENSE +21 -0
- package/README.en.md +149 -0
- package/README.md +108 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +1036 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +904 -0
- package/lib/types/index.d.ts +19 -0
- package/lib/types/server/remote.d.ts +117 -0
- package/lib/types/server/rpc.d.ts +4 -0
- package/lib/types/server/runner.d.ts +114 -0
- package/lib/types/server/settings.d.ts +24 -0
- package/lib/types/shared/contract.d.ts +87 -0
- package/package.json +108 -0
- package/src/client/api.ts +76 -0
- package/src/client/header.tsx +534 -0
- package/src/client/index.tsx +50 -0
- package/src/client/locales.ts +106 -0
- package/src/client/quick-commands.css +580 -0
- package/src/client/run-state.ts +125 -0
- package/src/client/settings-card.tsx +209 -0
- package/src/client/slot-augment.d.ts +14 -0
- package/src/index.ts +61 -0
- package/src/server/remote.ts +304 -0
- package/src/server/rpc.ts +286 -0
- package/src/server/runner.ts +471 -0
- package/src/server/settings.ts +106 -0
- package/src/shared/contract.ts +92 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fonlan/dsh-quick-commands host half: per-workspace quick commands.
|
|
3
|
+
*
|
|
4
|
+
* Registers the `quick-commands` settings namespace (workspaceId → command
|
|
5
|
+
* sets + popup anchor), owns the subprocess runner that executes commands with
|
|
6
|
+
* a fixed workspace cwd, and serves the fenced JSON API the client calls for
|
|
7
|
+
* config CRUD and live command output.
|
|
8
|
+
*
|
|
9
|
+
* Remote workspaces: when the workspace path is a @dsh-ssh/dsh-ssh placeholder
|
|
10
|
+
* directory and the `sshPool` service is present, commands stream over SSH on
|
|
11
|
+
* the remote host (same tail/offset semantics as local runs). The SSH backend
|
|
12
|
+
* is resolved lazily per run, so plugin load order is irrelevant and the
|
|
13
|
+
* absence of dsh-ssh degrades cleanly to local-only behaviour.
|
|
14
|
+
*/
|
|
15
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
16
|
+
export declare const name = "@fonlan/dsh-quick-commands";
|
|
17
|
+
export declare const inject: string[];
|
|
18
|
+
/** No plugin-level config; all configuration lives in the settings namespace. */
|
|
19
|
+
export declare function apply(ctx: Context): void;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/** Remote host id + remote working directory decoded from a placeholder path. */
|
|
2
|
+
export interface RemoteWorkspaceRef {
|
|
3
|
+
hostId: string;
|
|
4
|
+
/** Remote absolute path of the workspace root. */
|
|
5
|
+
remotePath: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function remoteRoot(env?: NodeJS.ProcessEnv): string;
|
|
8
|
+
/**
|
|
9
|
+
* Local absolute path → { hostId, remotePath } when it is a dsh-ssh
|
|
10
|
+
* placeholder (`<root>/<hostId>/<encoded>`), otherwise null. A coincidentally
|
|
11
|
+
* matching real directory is never misdetected: the encoded segment must
|
|
12
|
+
* round-trip to an absolute POSIX path.
|
|
13
|
+
*/
|
|
14
|
+
export declare function localPathToRemoteRef(localPath: string, env?: NodeJS.ProcessEnv): RemoteWorkspaceRef | null;
|
|
15
|
+
/**
|
|
16
|
+
* Map a session cwd (which may be inside the placeholder workspace, e.g. a
|
|
17
|
+
* subdirectory of the workspace root) onto the remote filesystem. Only paths
|
|
18
|
+
* strictly inside the placeholder are re-anchored; anything else falls back to
|
|
19
|
+
* the workspace's remote root.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveRemoteCwd(sessionCwd: string, workspaceLocalPath: string, remotePath: string): string;
|
|
22
|
+
/** Streaming chunk the remote exec yields before the final exit-code item. */
|
|
23
|
+
export interface RemoteExecChunk {
|
|
24
|
+
stream: 'stdout' | 'stderr';
|
|
25
|
+
chunk: Buffer;
|
|
26
|
+
}
|
|
27
|
+
/** The terminal item yielded by execStream: remote channel exit code. */
|
|
28
|
+
export interface RemoteExecExit {
|
|
29
|
+
exitCode: number;
|
|
30
|
+
}
|
|
31
|
+
export type RemoteExecEvent = RemoteExecChunk | RemoteExecExit;
|
|
32
|
+
/** SshConn minimal surface (see dsh-ssh ssh-core SshConn). */
|
|
33
|
+
export interface SshConnLike {
|
|
34
|
+
execStream(cmd: string, opts?: {
|
|
35
|
+
cwd?: string;
|
|
36
|
+
timeoutMs?: number;
|
|
37
|
+
}): AsyncGenerator<RemoteExecEvent>;
|
|
38
|
+
exec(cmd: string, opts?: {
|
|
39
|
+
cwd?: string;
|
|
40
|
+
timeoutMs?: number;
|
|
41
|
+
}): Promise<{
|
|
42
|
+
code: number;
|
|
43
|
+
signal: string | null;
|
|
44
|
+
stdout: string;
|
|
45
|
+
stderr: string;
|
|
46
|
+
}>;
|
|
47
|
+
}
|
|
48
|
+
/** SshPool minimal surface (see dsh-ssh ssh-core SshPool). */
|
|
49
|
+
export interface SshPoolLike {
|
|
50
|
+
acquire(cfg: {
|
|
51
|
+
id: string;
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
}): Promise<SshConnLike>;
|
|
54
|
+
release(): void;
|
|
55
|
+
}
|
|
56
|
+
/** Error carrying a SSH-stage tag (mirrors dsh-ssh SshError.stage). */
|
|
57
|
+
export interface SshStageError {
|
|
58
|
+
stage?: string;
|
|
59
|
+
message?: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Read one host's connection config from the `dsh-ssh-hosts` settings
|
|
63
|
+
* namespace (id → HostConfig dict, as written by the @dsh-ssh/dsh-ssh
|
|
64
|
+
* settings page). Returns `{ ...host, id }` for pool.acquire, or undefined
|
|
65
|
+
* when the host is not configured. `get` is the settings.get(ns) reader.
|
|
66
|
+
*/
|
|
67
|
+
export declare function readHostConfig(get: (ns: string) => unknown, hostId: string): {
|
|
68
|
+
id: string;
|
|
69
|
+
[key: string]: unknown;
|
|
70
|
+
} | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* One remote execution stream. Consumes conn.execStream in the background,
|
|
73
|
+
* appends decoded chunks into the caller-provided tail buffers, and records the
|
|
74
|
+
* terminal outcome.
|
|
75
|
+
*
|
|
76
|
+
* The command runs on the remote shell that sshd spawns for the channel (that
|
|
77
|
+
* shell is the session/process-group leader; `$$` is its pid AND its pgid). The
|
|
78
|
+
* leader pid is probed by prefixing `echo <marker> $$ >&2` to the command, so
|
|
79
|
+
* `kill` can terminate the WHOLE process group (`kill -TERM -- -<pid>`), which
|
|
80
|
+
* `pkill -f` alone cannot do for a child like `sleep 30` whose cmdline does not
|
|
81
|
+
* match the original command text. The probe line is filtered out of the stderr
|
|
82
|
+
* stream the user sees (a short buffer handles chunk-cut lines).
|
|
83
|
+
*/
|
|
84
|
+
export declare class RemoteStreamExecutor {
|
|
85
|
+
private readonly conn;
|
|
86
|
+
private readonly pool;
|
|
87
|
+
private cmd;
|
|
88
|
+
private cwd;
|
|
89
|
+
private started;
|
|
90
|
+
/** Remote process-group leader pid (parse the marker line; null until seen). */
|
|
91
|
+
private leaderPid;
|
|
92
|
+
/** Raw stderr head buffer for the (possibly chunk-cut) marker line. */
|
|
93
|
+
private markerBuf;
|
|
94
|
+
constructor(conn: SshConnLike, pool: SshPoolLike);
|
|
95
|
+
/**
|
|
96
|
+
* Start consuming the remote stream, settling the returned promise when the
|
|
97
|
+
* stream ends. `onData` sees decoded text chunks, `onExit` the terminal
|
|
98
|
+
* outcome (called exactly once, also on connection failure).
|
|
99
|
+
*/
|
|
100
|
+
start(cmd: string, cwd: string, onData: (stream: 'stdout' | 'stderr', text: string) => void, onExit: (exit: {
|
|
101
|
+
exitCode: number | null;
|
|
102
|
+
signal: string | null;
|
|
103
|
+
error?: string;
|
|
104
|
+
}) => void): Promise<void>;
|
|
105
|
+
/** Route stderr text, swallowing the (possibly split) marker line. */
|
|
106
|
+
private emitStderr;
|
|
107
|
+
/** The probed group-leader pid, if the marker line has been seen. */
|
|
108
|
+
get pid(): number | null;
|
|
109
|
+
/**
|
|
110
|
+
* Terminate the remote command's whole process group (best-effort). With the
|
|
111
|
+
* probed leader pid this is a true group kill (covers children like sleep);
|
|
112
|
+
* before the probe arrives (or when it never does) it falls back to a
|
|
113
|
+
* `pkill -f` on the exact `cd <cwd> && <cmd>` channel command line. The
|
|
114
|
+
* original stream then closes; the caller records signal 'TERM'.
|
|
115
|
+
*/
|
|
116
|
+
kill(): Promise<void>;
|
|
117
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { QuickCommandsSettingsFace } from './settings.js';
|
|
3
|
+
import type { QuickRunner } from './runner.js';
|
|
4
|
+
export declare function registerApiRoutes(ctx: Context, runner: QuickRunner, settings: QuickCommandsSettingsFace): () => void;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess';
|
|
2
|
+
import type { QuickCommandsSettings } from '../shared/contract.js';
|
|
3
|
+
import { type SshPoolLike } from './remote.js';
|
|
4
|
+
/** Optional SSH backend (duck-typed over the @dsh-ssh/dsh-ssh plugin). */
|
|
5
|
+
export interface RemoteBackend {
|
|
6
|
+
pool: SshPoolLike;
|
|
7
|
+
/** Resolve one hostId to its connection config (settings `dsh-ssh-hosts`). */
|
|
8
|
+
resolveHost: (hostId: string) => {
|
|
9
|
+
id: string;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
} | undefined;
|
|
12
|
+
}
|
|
13
|
+
/** Lazily-resolved SSH backend: resolved per run, so load order is irrelevant. */
|
|
14
|
+
export type RemoteBackendResolver = () => RemoteBackend | undefined;
|
|
15
|
+
/** A snapshot reader over one run's captured output (delta + offsets). */
|
|
16
|
+
export interface RunPollResult {
|
|
17
|
+
kind: 'poll';
|
|
18
|
+
status: 'running' | 'exited';
|
|
19
|
+
exitCode: number | null;
|
|
20
|
+
signal: string | null;
|
|
21
|
+
stdoutEnd: number;
|
|
22
|
+
stderrEnd: number;
|
|
23
|
+
stdoutDelta: string;
|
|
24
|
+
stderrDelta: string;
|
|
25
|
+
stdoutLossy: boolean;
|
|
26
|
+
stderrLossy: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** The runner's exported operations. */
|
|
29
|
+
export interface QuickRunner {
|
|
30
|
+
/**
|
|
31
|
+
* Start one command for a workspace (serial: rejects when one is live).
|
|
32
|
+
* Async: a remote run acquires its SSH connection before returning.
|
|
33
|
+
*/
|
|
34
|
+
start(input: {
|
|
35
|
+
workspaceId: string;
|
|
36
|
+
workspacePath: string;
|
|
37
|
+
workspaceTitle: string;
|
|
38
|
+
commandName: string;
|
|
39
|
+
command: string;
|
|
40
|
+
sessionCwd: string;
|
|
41
|
+
popupAnchor: QuickCommandsSettings['popupAnchor'];
|
|
42
|
+
}): Promise<{
|
|
43
|
+
ok: true;
|
|
44
|
+
runId: string;
|
|
45
|
+
remoteHost?: string;
|
|
46
|
+
resolvedCommand: string;
|
|
47
|
+
} | {
|
|
48
|
+
ok: false;
|
|
49
|
+
error: string;
|
|
50
|
+
}>;
|
|
51
|
+
/** Read output deltas since the caller's offsets. */
|
|
52
|
+
poll(runId: string, stdoutFrom: number, stderrFrom: number): RunPollResult | null;
|
|
53
|
+
/** Terminate the live process tree (idempotent). */
|
|
54
|
+
kill(runId: string): 'ok' | 'not-found' | 'already-exited';
|
|
55
|
+
/** List live runs for the workspace (for reattach / run list). */
|
|
56
|
+
forWorkspace(workspaceId: string): Array<{
|
|
57
|
+
runId: string;
|
|
58
|
+
commandName: string;
|
|
59
|
+
startedAt: number;
|
|
60
|
+
status: 'running' | 'exited';
|
|
61
|
+
}>;
|
|
62
|
+
}
|
|
63
|
+
/** Substitute supported placeholders in the command string. */
|
|
64
|
+
export declare function resolvePlaceholders(command: string, vars: {
|
|
65
|
+
workspace: string;
|
|
66
|
+
cwd: string;
|
|
67
|
+
title: string;
|
|
68
|
+
}): string;
|
|
69
|
+
/** Pick the shell executable for the current platform. */
|
|
70
|
+
export declare function shellCmd(): string;
|
|
71
|
+
/** Pick the `-c` inclusive switch for the current platform. */
|
|
72
|
+
export declare function shellFlag(): '-c';
|
|
73
|
+
interface SubprocessServiceLike {
|
|
74
|
+
spawn(spec: SubprocessSpawnSpec): SubprocessHandle;
|
|
75
|
+
}
|
|
76
|
+
export declare class QuickCommandRunner implements QuickRunner {
|
|
77
|
+
private readonly subprocess;
|
|
78
|
+
private readonly resolveRemote?;
|
|
79
|
+
private readonly runs;
|
|
80
|
+
private readonly liveByWorkspace;
|
|
81
|
+
constructor(subprocess: SubprocessServiceLike, resolveRemote?: RemoteBackendResolver | undefined);
|
|
82
|
+
/** Resolve the remote reference lazily so SSH availability is re-checked per run. */
|
|
83
|
+
private resolveRemoteBackend;
|
|
84
|
+
start(input: {
|
|
85
|
+
workspaceId: string;
|
|
86
|
+
workspacePath: string;
|
|
87
|
+
workspaceTitle: string;
|
|
88
|
+
commandName: string;
|
|
89
|
+
command: string;
|
|
90
|
+
sessionCwd: string;
|
|
91
|
+
popupAnchor: QuickCommandsSettings['popupAnchor'];
|
|
92
|
+
}): Promise<{
|
|
93
|
+
ok: true;
|
|
94
|
+
runId: string;
|
|
95
|
+
remoteHost?: string;
|
|
96
|
+
resolvedCommand: string;
|
|
97
|
+
} | {
|
|
98
|
+
ok: false;
|
|
99
|
+
error: string;
|
|
100
|
+
}>;
|
|
101
|
+
private startLocal;
|
|
102
|
+
private startRemote;
|
|
103
|
+
poll(runId: string, stdoutFrom: number, stderrFrom: number): RunPollResult | null;
|
|
104
|
+
kill(runId: string): 'ok' | 'not-found' | 'already-exited';
|
|
105
|
+
forWorkspace(workspaceId: string): Array<{
|
|
106
|
+
runId: string;
|
|
107
|
+
commandName: string;
|
|
108
|
+
startedAt: number;
|
|
109
|
+
status: 'running' | 'exited';
|
|
110
|
+
}>;
|
|
111
|
+
/** Every live run id (terminal and running) for fiber cleanup. */
|
|
112
|
+
listAll(): string[];
|
|
113
|
+
}
|
|
114
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-facing plugin settings (per-workspace command sets + popup anchor),
|
|
3
|
+
* persisted through the settings service namespace `quick-commands`.
|
|
4
|
+
* The DSH settings service requires a lowercase kebab-case namespace
|
|
5
|
+
* (/^[a-z][a-z0-9-]*$/), so the scoped package name cannot be used here.
|
|
6
|
+
*/
|
|
7
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
8
|
+
import z from '@deepseek-ai/schemastery';
|
|
9
|
+
import type { QuickCommandsSettings, QuickCommandEntry, QuickPopupSize } from '../shared/contract.js';
|
|
10
|
+
export declare const QUICK_COMMANDS_NS: import("@deepseek-ai/dsh-settings").SettingsNamespace;
|
|
11
|
+
export declare const QuickCommandsSettingsSchema: z<QuickCommandsSettings>;
|
|
12
|
+
/** Settings service face for the quick-commands engine. */
|
|
13
|
+
export interface QuickCommandsSettingsFace {
|
|
14
|
+
get(): QuickCommandsSettings;
|
|
15
|
+
/** Read the command set bound to one workspace id. */
|
|
16
|
+
commandsOf(workspaceId: string): QuickCommandEntry[];
|
|
17
|
+
/** Replace the whole command set for one workspace id (instant-save). */
|
|
18
|
+
setCommands(workspaceId: string, commands: QuickCommandEntry[]): Promise<void>;
|
|
19
|
+
/** Set the popup anchor preference. */
|
|
20
|
+
setAnchor(anchor: QuickCommandsSettings['popupAnchor']): Promise<void>;
|
|
21
|
+
/** Persist the output popup size (drag-resize). */
|
|
22
|
+
setPopupSize(size: QuickPopupSize): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export declare function registerSettings(ctx: Context): QuickCommandsSettingsFace;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared wire contracts for @fonlan/dsh-quick-commands.
|
|
3
|
+
*
|
|
4
|
+
* Browser-safe: no host-only imports live here. The client bundle imports
|
|
5
|
+
* these shapes (type-only + runtime constants), so anything that must cross
|
|
6
|
+
* the fenced JSON API keeps its plain-Object shape here.
|
|
7
|
+
*/
|
|
8
|
+
/** One configured quick command inside a workspace. */
|
|
9
|
+
export interface QuickCommandEntry {
|
|
10
|
+
/** Human-facing command name (unique within the workspace). */
|
|
11
|
+
name: string;
|
|
12
|
+
/** Shell command line, executed through bash -c / pwsh -c. */
|
|
13
|
+
command: string;
|
|
14
|
+
}
|
|
15
|
+
/** Config slice bound to one workspace (keyed by workspaceId). */
|
|
16
|
+
export interface WorkspaceCommands {
|
|
17
|
+
/** DSH workspace registry id this command set belongs to. */
|
|
18
|
+
workspaceId: string;
|
|
19
|
+
/** Ordered command entries. */
|
|
20
|
+
commands: QuickCommandEntry[];
|
|
21
|
+
}
|
|
22
|
+
/** Output popup dimensions in CSS pixels (user drag-resize state). */
|
|
23
|
+
export interface QuickPopupSize {
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
}
|
|
27
|
+
/** Full user-facing plugin settings document (settings namespace value). */
|
|
28
|
+
export interface QuickCommandsSettings {
|
|
29
|
+
/** Command sets; a workspace may be absent (no commands configured). */
|
|
30
|
+
workspaces: WorkspaceCommands[];
|
|
31
|
+
/** Output popup anchor preference: fixed bottom-right or anchored at the button. */
|
|
32
|
+
popupAnchor: 'corner' | 'button';
|
|
33
|
+
/** Output popup size; absent until the user drags the resize handle (then per-anchor defaults apply). */
|
|
34
|
+
popupSize?: QuickPopupSize;
|
|
35
|
+
}
|
|
36
|
+
/** Placeholder substitution values resolved for one execution. */
|
|
37
|
+
export interface QuickRunPlaceholders {
|
|
38
|
+
/** Workspace root path (also the execution cwd). */
|
|
39
|
+
workspace: string;
|
|
40
|
+
/** Session actual cwd (may be a subdirectory of the workspace). */
|
|
41
|
+
cwd: string;
|
|
42
|
+
/** Workspace display title. */
|
|
43
|
+
title: string;
|
|
44
|
+
}
|
|
45
|
+
/** One live run view returned by `run.start`. */
|
|
46
|
+
export interface QuickRunView {
|
|
47
|
+
runId: string;
|
|
48
|
+
workspaceId: string;
|
|
49
|
+
commandName: string;
|
|
50
|
+
/** Command line after placeholder substitution (display only). */
|
|
51
|
+
resolvedCommand: string;
|
|
52
|
+
/** Non-empty when the command executes over SSH on a remote workspace host. */
|
|
53
|
+
remoteHost?: string;
|
|
54
|
+
status: 'running' | 'exited';
|
|
55
|
+
exitCode: number | null;
|
|
56
|
+
signal: string | null;
|
|
57
|
+
startedAt: number;
|
|
58
|
+
/** Total stdout bytes captured so far (offset currency). */
|
|
59
|
+
stdoutEnd: number;
|
|
60
|
+
/** Total stderr bytes captured so far (offset currency). */
|
|
61
|
+
stderrEnd: number;
|
|
62
|
+
/** stdout tail appended since `stdoutFrom` (whole tail when lossy). */
|
|
63
|
+
stdoutDelta: string;
|
|
64
|
+
/** stderr tail appended since `stderrFrom` (whole tail when lossy). */
|
|
65
|
+
stderrDelta: string;
|
|
66
|
+
/** True when the in-memory tail lost its head for this stream. */
|
|
67
|
+
stdoutLossy: boolean;
|
|
68
|
+
stderrLossy: boolean;
|
|
69
|
+
}
|
|
70
|
+
/** API response envelope (mirrors task-kanban's fenced fetch wrapper). */
|
|
71
|
+
export interface ApiEnvelope<T> {
|
|
72
|
+
ok: boolean;
|
|
73
|
+
value?: T;
|
|
74
|
+
error?: {
|
|
75
|
+
code: string;
|
|
76
|
+
message: string;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Settings card view: command sets plus the workspace roster for display. */
|
|
80
|
+
export interface QuickRosterEntry {
|
|
81
|
+
workspaceId: string;
|
|
82
|
+
path: string;
|
|
83
|
+
title: string;
|
|
84
|
+
/** SSH host id when the workspace runs remotely through dsh-ssh (else absent). */
|
|
85
|
+
remoteHost?: string;
|
|
86
|
+
commands: QuickCommandEntry[];
|
|
87
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fonlan/dsh-quick-commands",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DSH web plugin: per-workspace quick commands. Configure named shell commands per DSH workspace, run one from the session header with live stdout/stderr in a floating popup.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/fonlan/dsh-quick-commands.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/fonlan/dsh-quick-commands/issues"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/fonlan/dsh-quick-commands",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"dsh",
|
|
16
|
+
"quick-commands",
|
|
17
|
+
"plugin",
|
|
18
|
+
"workspace",
|
|
19
|
+
"commands"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "lib/index.js",
|
|
23
|
+
"types": "lib/types/index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./lib/types/index.d.ts",
|
|
27
|
+
"default": "./lib/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./client": {
|
|
30
|
+
"types": "./lib/types/client/index.d.ts",
|
|
31
|
+
"default": "./lib/client.js"
|
|
32
|
+
},
|
|
33
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"lib",
|
|
37
|
+
"src",
|
|
38
|
+
"cordis.patch.yml",
|
|
39
|
+
"README.md",
|
|
40
|
+
"README.en.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "rm -rf lib && tsc -p tsconfig.build.json && tsdown",
|
|
48
|
+
"build:client": "tsdown",
|
|
49
|
+
"prepare": "npm run build",
|
|
50
|
+
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.client.json",
|
|
51
|
+
"bundle": "tsdown",
|
|
52
|
+
"watch": "tsdown --watch",
|
|
53
|
+
"release:patch": "npm version patch && git push && git push --tags",
|
|
54
|
+
"release:minor": "npm version minor && git push && git push --tags",
|
|
55
|
+
"release:major": "npm version major && git push && git push --tags"
|
|
56
|
+
},
|
|
57
|
+
"dsh": {
|
|
58
|
+
"bundle": {
|
|
59
|
+
"patch": "./cordis.patch.yml"
|
|
60
|
+
},
|
|
61
|
+
"client": {
|
|
62
|
+
"platform": "web",
|
|
63
|
+
"inject": [
|
|
64
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
65
|
+
"@deepseek-ai/dsh-client-locale",
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-primitives",
|
|
68
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
69
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
70
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
71
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins"
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"peerDependencies": {
|
|
76
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
77
|
+
"@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
|
|
78
|
+
"@deepseek-ai/dsh-subprocess": "^0.1.1-rc.2",
|
|
79
|
+
"@deepseek-ai/dsh-workspace": "^0.1.1-rc.2",
|
|
80
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.2",
|
|
81
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
82
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
|
|
83
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.1-rc.2",
|
|
84
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2",
|
|
85
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.1-rc.2",
|
|
86
|
+
"react": "^18.2.0",
|
|
87
|
+
"react-dom": "^18.2.0"
|
|
88
|
+
},
|
|
89
|
+
"devDependencies": {
|
|
90
|
+
"@types/node": "^22.0.0",
|
|
91
|
+
"@types/react": "~18.3.1",
|
|
92
|
+
"@types/react-dom": "~18.3.0",
|
|
93
|
+
"tsdown": "^0.22.14",
|
|
94
|
+
"typescript": "^5.9.0",
|
|
95
|
+
"@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
|
|
96
|
+
"@deepseek-ai/dsh-subprocess": "^0.1.1-rc.2",
|
|
97
|
+
"@deepseek-ai/dsh-workspace": "^0.1.1-rc.2",
|
|
98
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.2",
|
|
99
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
100
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
|
|
101
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.1-rc.2",
|
|
102
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2",
|
|
103
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.1-rc.2",
|
|
104
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.1-rc.2",
|
|
105
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.1-rc.2"
|
|
106
|
+
},
|
|
107
|
+
"packageManager": "pnpm@11.21.0"
|
|
108
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser client for the plugin's fenced JSON API
|
|
3
|
+
* (/plugins/@fonlan/dsh-quick-commands/api/<method>). Same-origin fetch with
|
|
4
|
+
* the task-kanban envelope shape: { ok, value } | { ok:false, error }.
|
|
5
|
+
*/
|
|
6
|
+
import type {
|
|
7
|
+
ApiEnvelope,
|
|
8
|
+
QuickCommandsSettings,
|
|
9
|
+
QuickPopupSize,
|
|
10
|
+
QuickRosterEntry,
|
|
11
|
+
QuickRunView,
|
|
12
|
+
} from '../shared/contract'
|
|
13
|
+
|
|
14
|
+
export class QuickCommandsApiError extends Error {
|
|
15
|
+
constructor(readonly code: string, message: string) {
|
|
16
|
+
super(message)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function call<T>(method: string, payload: Record<string, unknown> = {}): Promise<T> {
|
|
21
|
+
let response: Response
|
|
22
|
+
try {
|
|
23
|
+
response = await fetch(`/plugins/@fonlan/dsh-quick-commands/api/${method}`, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: { 'content-type': 'application/json' },
|
|
26
|
+
body: JSON.stringify(payload),
|
|
27
|
+
})
|
|
28
|
+
} catch (error) {
|
|
29
|
+
throw new QuickCommandsApiError('network', error instanceof Error ? error.message : String(error))
|
|
30
|
+
}
|
|
31
|
+
const parsed = (await response.json().catch(() => null)) as ApiEnvelope<T> | null
|
|
32
|
+
if (!response.ok || parsed === null || parsed.ok !== true || parsed.value === undefined) {
|
|
33
|
+
throw new QuickCommandsApiError(
|
|
34
|
+
(parsed?.error?.code as string | undefined) ?? 'http',
|
|
35
|
+
parsed?.error?.message ?? `HTTP ${response.status}`,
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
return parsed.value as T
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Current session cwd of a session row (SessionSummary carries cwd). */
|
|
42
|
+
export interface SessionRow {
|
|
43
|
+
id: string
|
|
44
|
+
cwd?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** One delta read result (mirrors the host RunPollResult). */
|
|
48
|
+
export interface RunPoll {
|
|
49
|
+
status: 'running' | 'exited'
|
|
50
|
+
exitCode: number | null
|
|
51
|
+
signal: string | null
|
|
52
|
+
stdoutEnd: number
|
|
53
|
+
stderrEnd: number
|
|
54
|
+
stdoutDelta: string
|
|
55
|
+
stderrDelta: string
|
|
56
|
+
stdoutLossy: boolean
|
|
57
|
+
stderrLossy: boolean
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const quickApi = {
|
|
61
|
+
settingsGet: () => call<QuickCommandsSettings>('settings.get'),
|
|
62
|
+
settingsSetCommands: (workspaceId: string, commands: Array<{ name: string; command: string }>) =>
|
|
63
|
+
call<QuickCommandsSettings>('settings.setCommands', { workspaceId, commands }),
|
|
64
|
+
settingsSetAnchor: (anchor: QuickCommandsSettings['popupAnchor']) =>
|
|
65
|
+
call<QuickCommandsSettings>('settings.setAnchor', { anchor }),
|
|
66
|
+
settingsSetPopupSize: (size: QuickPopupSize) =>
|
|
67
|
+
call<QuickCommandsSettings>('settings.setPopupSize', { width: size.width, height: size.height }),
|
|
68
|
+
workspacesList: () => call<QuickRosterEntry[]>('workspaces.list'),
|
|
69
|
+
runStart: (workspaceId: string, commandName: string, sessionCwd: string) =>
|
|
70
|
+
call<QuickRunView>('run.start', { workspaceId, commandName, sessionCwd }),
|
|
71
|
+
runPoll: (runId: string, stdoutFrom: number, stderrFrom: number) =>
|
|
72
|
+
call<RunPoll>('run.poll', { runId, stdoutFrom, stderrFrom }),
|
|
73
|
+
runKill: (runId: string) => call<{ killed: boolean }>('run.kill', { runId }),
|
|
74
|
+
runList: (workspaceId: string) =>
|
|
75
|
+
call<Array<{ runId: string; commandName: string; startedAt: number; status: 'running' | 'exited' }>>('run.list', { workspaceId }),
|
|
76
|
+
}
|