paseo-dsh-direct 0.1.4

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.
@@ -0,0 +1,71 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ const DEFAULT_TIMEOUT_MS = 3_000;
4
+
5
+ /** One probe per executable path per process; repeated session opens reuse it instead of spawning again. */
6
+ const cache = new Map<string, Promise<string>>();
7
+
8
+ export interface DetectDshVersionOptions {
9
+ timeoutMs?: number;
10
+ env?: Readonly<Record<string, string | undefined>>;
11
+ }
12
+
13
+ /**
14
+ * Runs `<executable> --version` and returns the version it printed. Returns
15
+ * `""` when the process exits non-zero, times out, or prints nothing a
16
+ * version can be parsed from — callers treat `""` as "unconfirmed" via
17
+ * `matchVersionLine`, never as a reason to block startup. Cached by
18
+ * executable path for the life of the process.
19
+ */
20
+ export function detectDshVersion(executable: string, options: DetectDshVersionOptions = {}): Promise<string> {
21
+ const cached = cache.get(executable);
22
+ if (cached) return cached;
23
+ const probe = runVersionProbe(executable, options);
24
+ cache.set(executable, probe);
25
+ return probe;
26
+ }
27
+
28
+ function runVersionProbe(executable: string, options: DetectDshVersionOptions): Promise<string> {
29
+ return new Promise((resolve) => {
30
+ let settled = false;
31
+ let timeout: ReturnType<typeof setTimeout> | undefined;
32
+ const finish = (value: string) => {
33
+ if (settled) return;
34
+ settled = true;
35
+ if (timeout) clearTimeout(timeout);
36
+ resolve(value);
37
+ };
38
+
39
+ let child;
40
+ try {
41
+ child = spawn(executable, ["--version"], {
42
+ env: options.env ?? process.env,
43
+ stdio: ["ignore", "pipe", "ignore"],
44
+ windowsHide: true,
45
+ });
46
+ } catch {
47
+ // Synchronous spawn failure (e.g. the executable does not exist).
48
+ finish("");
49
+ return;
50
+ }
51
+
52
+ timeout = setTimeout(() => {
53
+ child.kill("SIGKILL");
54
+ finish("");
55
+ }, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
56
+
57
+ let stdout = "";
58
+ child.stdout?.setEncoding("utf8");
59
+ child.stdout?.on("data", (chunk: string) => {
60
+ stdout += chunk;
61
+ });
62
+ child.once("error", () => finish(""));
63
+ child.once("exit", (code) => finish(code === 0 ? extractVersion(stdout) : ""));
64
+ });
65
+ }
66
+
67
+ /** The first semver-looking token in the output; dsh prints only the version, but tolerate extra text. */
68
+ function extractVersion(output: string): string {
69
+ const match = /\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?/.exec(output);
70
+ return match ? match[0] : "";
71
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * This plugin's own version, taken from `package.json` by `npm run build`
3
+ * (Paseo refuses plugin modules outside client/, server/ and shared/, so it
4
+ * cannot be imported at runtime) — the single source for the dsh version
5
+ * line it supports (see `versionLine` in `./version-line.js`).
6
+ */
7
+ export { PLUGIN_VERSION } from "./generated-version.js";
@@ -0,0 +1,120 @@
1
+ import type { ProviderContent } from "@getpaseo/plugin/server/provider";
2
+ import type { PromptPart } from "../shared/bridge-protocol.js";
3
+
4
+ const REVIEW_LINE_MARKERS = { add: "+", remove: "-", context: " " } as const;
5
+
6
+ /** Map Paseo prompt content to bridge parts; context attachments become text as in Paseo's own providers. */
7
+ export function toPromptParts(content: readonly ProviderContent[]): PromptPart[] {
8
+ return content.map((part): PromptPart => {
9
+ switch (part.type) {
10
+ case "text":
11
+ // Also covers Paseo text attachments, which share the `text` shape.
12
+ return { type: "text", text: part.text };
13
+ case "image":
14
+ return { type: "image", mediaType: part.mimeType, data: part.data };
15
+ case "uploaded_file":
16
+ return { type: "file", path: part.path, name: part.fileName };
17
+ default:
18
+ return { type: "text", text: attachmentText(part) };
19
+ }
20
+ });
21
+ }
22
+
23
+ /** Text shown in the Paseo user bubble for a submitted prompt. */
24
+ export function displayText(parts: readonly PromptPart[]): string {
25
+ return parts
26
+ .map((part) => (part.type === "text" ? part.text : attachmentLabel(part.type, part.name)))
27
+ .filter(Boolean)
28
+ .join("\n");
29
+ }
30
+
31
+ /** Bubble marker for a non-text block; shared by live prompts and replayed history. */
32
+ export function attachmentLabel(type: "image" | "file", name?: string): string {
33
+ return type === "image" ? "[image]" : `[file: ${name ?? "attachment"}]`;
34
+ }
35
+
36
+ type ContextAttachment = Exclude<
37
+ ProviderContent,
38
+ { type: "text" } | { type: "image" } | { type: "uploaded_file" }
39
+ >;
40
+
41
+ function attachmentText(part: ContextAttachment): string {
42
+ switch (part.type) {
43
+ case "forge_change_request":
44
+ return changeRequestText({ ...part, forge: part.forge ?? "github" });
45
+ case "github_pr":
46
+ return changeRequestText({ ...part, forge: "github" });
47
+ case "forge_issue":
48
+ return issueText({ ...part, forge: part.forge ?? "github" });
49
+ case "github_issue":
50
+ return issueText({ ...part, forge: "github" });
51
+ case "review": {
52
+ const lines = [`Paseo review attachment (${part.mode})`, `CWD: ${part.cwd}`];
53
+ if (part.baseRef) lines.push(`Base: ${part.baseRef}`);
54
+ part.comments.forEach((comment, index) => {
55
+ lines.push(
56
+ "",
57
+ `Comment ${index + 1}: ${comment.filePath}:${comment.side}:${comment.lineNumber}`,
58
+ comment.body,
59
+ comment.context.hunkHeader,
60
+ );
61
+ const target = comment.context.targetLine;
62
+ for (const line of comment.context.lines) {
63
+ const isTarget =
64
+ line.oldLineNumber === target.oldLineNumber &&
65
+ line.newLineNumber === target.newLineNumber &&
66
+ line.type === target.type &&
67
+ line.content === target.content;
68
+ lines.push(
69
+ `${isTarget ? "> " : " "}${pad(line.oldLineNumber)} ${pad(line.newLineNumber)} ${REVIEW_LINE_MARKERS[line.type]}${line.content}`,
70
+ );
71
+ }
72
+ });
73
+ return lines.join("\n");
74
+ }
75
+ }
76
+ }
77
+
78
+ function changeRequestText(input: {
79
+ forge: string;
80
+ number: number;
81
+ title: string;
82
+ url: string;
83
+ body?: string | null;
84
+ projectPath?: string;
85
+ baseRefName?: string | null;
86
+ headRefName?: string | null;
87
+ }): string {
88
+ const gitlab = input.forge === "gitlab";
89
+ const lines = [
90
+ `${forgeLabel(input.forge)} ${gitlab ? "MR" : "PR"} ${gitlab ? "!" : "#"}${input.number}: ${input.title}`,
91
+ input.url,
92
+ ];
93
+ if (input.projectPath) lines.push(`Project: ${input.projectPath}`);
94
+ if (input.baseRefName) lines.push(`Base: ${input.baseRefName}`);
95
+ if (input.headRefName) lines.push(`Head: ${input.headRefName}`);
96
+ if (input.body) lines.push("", input.body);
97
+ return lines.join("\n");
98
+ }
99
+
100
+ function issueText(input: {
101
+ forge: string;
102
+ number: number;
103
+ title: string;
104
+ url: string;
105
+ body?: string | null;
106
+ projectPath?: string;
107
+ }): string {
108
+ const lines = [`${forgeLabel(input.forge)} Issue #${input.number}: ${input.title}`, input.url];
109
+ if (input.projectPath) lines.push(`Project: ${input.projectPath}`);
110
+ if (input.body) lines.push("", input.body);
111
+ return lines.join("\n");
112
+ }
113
+
114
+ function forgeLabel(forge: string): string {
115
+ return forge === "gitlab" ? "GitLab" : forge === "github" ? "GitHub" : forge;
116
+ }
117
+
118
+ function pad(lineNumber: number | null): string {
119
+ return (lineNumber === null ? "" : String(lineNumber)).padStart(4, " ");
120
+ }