dsh-plugin-cc 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/.claude-plugin/marketplace.json +17 -0
- package/LICENSE +201 -0
- package/NOTICE +11 -0
- package/README.md +96 -0
- package/package.json +12 -0
- package/plugins/dsh/.claude-plugin/plugin.json +6 -0
- package/plugins/dsh/CHANGELOG.md +8 -0
- package/plugins/dsh/LICENSE +201 -0
- package/plugins/dsh/NOTICE +11 -0
- package/plugins/dsh/agents/dsh-rescue.md +41 -0
- package/plugins/dsh/commands/cancel.md +8 -0
- package/plugins/dsh/commands/plan.md +54 -0
- package/plugins/dsh/commands/rescue.md +51 -0
- package/plugins/dsh/commands/result.md +15 -0
- package/plugins/dsh/commands/review-plan.md +69 -0
- package/plugins/dsh/commands/setup.md +37 -0
- package/plugins/dsh/commands/status.md +17 -0
- package/plugins/dsh/hooks/hooks.json +27 -0
- package/plugins/dsh/prompts/plan.md +21 -0
- package/plugins/dsh/prompts/review-plan.md +25 -0
- package/plugins/dsh/scripts/dsh-companion.mjs +720 -0
- package/plugins/dsh/scripts/lib/args.mjs +130 -0
- package/plugins/dsh/scripts/lib/dsh.mjs +238 -0
- package/plugins/dsh/scripts/lib/fs.mjs +40 -0
- package/plugins/dsh/scripts/lib/git.mjs +14 -0
- package/plugins/dsh/scripts/lib/job-control.mjs +314 -0
- package/plugins/dsh/scripts/lib/plans.mjs +70 -0
- package/plugins/dsh/scripts/lib/process.mjs +156 -0
- package/plugins/dsh/scripts/lib/prompts.mjs +13 -0
- package/plugins/dsh/scripts/lib/render.mjs +231 -0
- package/plugins/dsh/scripts/lib/state.mjs +258 -0
- package/plugins/dsh/scripts/lib/tracked-jobs.mjs +242 -0
- package/plugins/dsh/scripts/lib/workspace.mjs +9 -0
- package/plugins/dsh/scripts/session-lifecycle-hook.mjs +116 -0
- package/plugins/dsh/skills/dsh-cli-runtime/SKILL.md +40 -0
- package/plugins/dsh/skills/dsh-result-handling/SKILL.md +19 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
export function parseArgs(argv, config = {}) {
|
|
2
|
+
const valueOptions = new Set(config.valueOptions ?? []);
|
|
3
|
+
const booleanOptions = new Set(config.booleanOptions ?? []);
|
|
4
|
+
const aliasMap = config.aliasMap ?? {};
|
|
5
|
+
const options = {};
|
|
6
|
+
const positionals = [];
|
|
7
|
+
let passthrough = false;
|
|
8
|
+
|
|
9
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
10
|
+
const token = argv[index];
|
|
11
|
+
|
|
12
|
+
if (passthrough) {
|
|
13
|
+
positionals.push(token);
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (token === "--") {
|
|
18
|
+
passthrough = true;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!token.startsWith("-") || token === "-") {
|
|
23
|
+
positionals.push(token);
|
|
24
|
+
// Free text must never be read as options: everything after the first plain word is text.
|
|
25
|
+
passthrough = config.stopAtPositional === true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (token.startsWith("--")) {
|
|
30
|
+
const [rawKey, inlineValue] = token.slice(2).split("=", 2);
|
|
31
|
+
const key = aliasMap[rawKey] ?? rawKey;
|
|
32
|
+
|
|
33
|
+
if (booleanOptions.has(key)) {
|
|
34
|
+
options[key] = inlineValue === undefined ? true : inlineValue !== "false";
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (valueOptions.has(key)) {
|
|
39
|
+
const nextValue = inlineValue ?? argv[index + 1];
|
|
40
|
+
if (nextValue === undefined) {
|
|
41
|
+
throw new Error(`Missing value for --${rawKey}`);
|
|
42
|
+
}
|
|
43
|
+
options[key] = nextValue;
|
|
44
|
+
if (inlineValue === undefined) {
|
|
45
|
+
index += 1;
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
positionals.push(token);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const shortKey = token.slice(1);
|
|
55
|
+
const key = aliasMap[shortKey] ?? shortKey;
|
|
56
|
+
|
|
57
|
+
if (booleanOptions.has(key)) {
|
|
58
|
+
options[key] = true;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (valueOptions.has(key)) {
|
|
63
|
+
const nextValue = argv[index + 1];
|
|
64
|
+
if (nextValue === undefined) {
|
|
65
|
+
throw new Error(`Missing value for -${shortKey}`);
|
|
66
|
+
}
|
|
67
|
+
options[key] = nextValue;
|
|
68
|
+
index += 1;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
positionals.push(token);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { options, positionals };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function splitRawArgumentString(raw) {
|
|
79
|
+
const tokens = [];
|
|
80
|
+
let current = "";
|
|
81
|
+
let quote = null;
|
|
82
|
+
let escaping = false;
|
|
83
|
+
|
|
84
|
+
for (const character of raw) {
|
|
85
|
+
if (escaping) {
|
|
86
|
+
current += character;
|
|
87
|
+
escaping = false;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (character === "\\") {
|
|
92
|
+
escaping = true;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (quote) {
|
|
97
|
+
if (character === quote) {
|
|
98
|
+
quote = null;
|
|
99
|
+
} else {
|
|
100
|
+
current += character;
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (character === "'" || character === "\"") {
|
|
106
|
+
quote = character;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (/\s/.test(character)) {
|
|
111
|
+
if (current) {
|
|
112
|
+
tokens.push(current);
|
|
113
|
+
current = "";
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
current += character;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (escaping) {
|
|
122
|
+
current += "\\";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (current) {
|
|
126
|
+
tokens.push(current);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return tokens;
|
|
130
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
|
|
6
|
+
import { createTempDir } from "./fs.mjs";
|
|
7
|
+
import { binaryAvailable } from "./process.mjs";
|
|
8
|
+
import { renderRunResult } from "./render.mjs";
|
|
9
|
+
|
|
10
|
+
export const CHILD_ENV = "DSH_COMPANION_CHILD";
|
|
11
|
+
export const DEFAULT_MODEL = "deepseek-flash";
|
|
12
|
+
export const SUPPORTED_RANGE = ">=0.2.0-rc.2 <0.3.0";
|
|
13
|
+
export const MODEL_ALIASES = { flash: "deepseek-flash", pro: "deepseek-v4-pro" };
|
|
14
|
+
export const PROVIDER = "deepseek-official";
|
|
15
|
+
export const PERMISSION_MODES = ["read-only", "workspace-write"];
|
|
16
|
+
|
|
17
|
+
export function normalizeModel(model) {
|
|
18
|
+
const value = typeof model === "string" ? model.trim() : "";
|
|
19
|
+
if (!value) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return MODEL_ALIASES[value.toLowerCase()] ?? value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// No preset may use `approval: never`. It auto-approves, and headless has no approval channel, so `ask` fails closed.
|
|
26
|
+
const PRESET_TABLE = [
|
|
27
|
+
" read-only: { sandbox: read-only, approval: ask }",
|
|
28
|
+
" workspace-write: { sandbox: workspace-write, approval: ask }",
|
|
29
|
+
" danger-full-access: { sandbox: danger-full-access, approval: ask }"
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Writes the per-run patch and returns its path. The sandbox mode and default preset are pinned
|
|
34
|
+
* to the requested permission mode; the model block is added only when a model is requested.
|
|
35
|
+
*/
|
|
36
|
+
export function writeRunPatch({ permissionMode, model, cwd }) {
|
|
37
|
+
if (!PERMISSION_MODES.includes(permissionMode)) {
|
|
38
|
+
throw new Error(`Unsupported permission mode "${permissionMode}". Use read-only or workspace-write.`);
|
|
39
|
+
}
|
|
40
|
+
const lines = [
|
|
41
|
+
"- id: sandbox-policy",
|
|
42
|
+
` config: { mode: ${permissionMode}, workspaceRoot: ${JSON.stringify(cwd)} }`,
|
|
43
|
+
"- id: permission",
|
|
44
|
+
" config:",
|
|
45
|
+
` defaultPreset: ${permissionMode}`,
|
|
46
|
+
" presets:",
|
|
47
|
+
...PRESET_TABLE
|
|
48
|
+
];
|
|
49
|
+
const resolvedModel = normalizeModel(model);
|
|
50
|
+
if (resolvedModel) {
|
|
51
|
+
lines.push("- id: agent-default-model", ` config: { provider: ${PROVIDER}, model: ${JSON.stringify(resolvedModel)} }`);
|
|
52
|
+
}
|
|
53
|
+
const dir = createTempDir("dsh-plugin-");
|
|
54
|
+
const file = path.join(dir, "run.patch.yml");
|
|
55
|
+
fs.writeFileSync(file, `${lines.join("\n")}\n`, "utf8");
|
|
56
|
+
return file;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function removePatch(file) {
|
|
60
|
+
try {
|
|
61
|
+
fs.rmSync(path.dirname(file), { recursive: true, force: true });
|
|
62
|
+
} catch {
|
|
63
|
+
// Best effort: the patch lives in the OS temp dir.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseEventLine(line) {
|
|
68
|
+
const trimmed = line.trim();
|
|
69
|
+
if (!trimmed.startsWith("{")) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const parsed = JSON.parse(trimmed);
|
|
74
|
+
return parsed && typeof parsed === "object" && typeof parsed.type === "string" ? parsed : null;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Runs one headless dsh process in its own process group.
|
|
82
|
+
* Returns the tracked-job runner contract.
|
|
83
|
+
*/
|
|
84
|
+
export function runHeadless({ cwd, prompt, permissionMode, model, sessionId, onSpawn, onEvent, timeoutMs }) {
|
|
85
|
+
const patchFile = writeRunPatch({ permissionMode, model, cwd });
|
|
86
|
+
const resolvedModel = normalizeModel(model) ?? DEFAULT_MODEL;
|
|
87
|
+
const args = ["--patch", patchFile, "--profile", "headless", "--json"];
|
|
88
|
+
if (sessionId) {
|
|
89
|
+
args.push("--session-id", sessionId);
|
|
90
|
+
}
|
|
91
|
+
args.push("-");
|
|
92
|
+
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const child = spawn("dsh", args, {
|
|
95
|
+
cwd,
|
|
96
|
+
detached: true,
|
|
97
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
98
|
+
env: { ...process.env, DSH_PERMISSION_MODE: permissionMode, [CHILD_ENV]: "1" }
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
let dshSessionId = sessionId ?? null;
|
|
102
|
+
let finalText = "";
|
|
103
|
+
let errorMessage = "";
|
|
104
|
+
let usage = null;
|
|
105
|
+
let stderr = "";
|
|
106
|
+
let buffer = "";
|
|
107
|
+
let settled = false;
|
|
108
|
+
let timer = null;
|
|
109
|
+
|
|
110
|
+
const finish = (fn) => {
|
|
111
|
+
if (settled) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
settled = true;
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
removePatch(patchFile);
|
|
117
|
+
fn();
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const handleEvent = (event) => {
|
|
121
|
+
if (event.type === "session" && typeof event.sessionId === "string") {
|
|
122
|
+
dshSessionId = event.sessionId;
|
|
123
|
+
} else if (event.type === "final" && typeof event.text === "string") {
|
|
124
|
+
finalText = event.text;
|
|
125
|
+
} else if (event.type === "error") {
|
|
126
|
+
errorMessage = String(event.message ?? "dsh reported an error");
|
|
127
|
+
} else if (event.type === "status" && event.usage) {
|
|
128
|
+
usage = event.usage;
|
|
129
|
+
}
|
|
130
|
+
onEvent?.(event, { dshSessionId });
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
child.on("error", (error) => {
|
|
134
|
+
finish(() => reject(error));
|
|
135
|
+
});
|
|
136
|
+
child.stdout.setEncoding("utf8");
|
|
137
|
+
child.stdout.on("data", (chunk) => {
|
|
138
|
+
buffer += chunk;
|
|
139
|
+
let index;
|
|
140
|
+
while ((index = buffer.indexOf("\n")) !== -1) {
|
|
141
|
+
const event = parseEventLine(buffer.slice(0, index));
|
|
142
|
+
buffer = buffer.slice(index + 1);
|
|
143
|
+
if (event) {
|
|
144
|
+
handleEvent(event);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
child.stderr.setEncoding("utf8");
|
|
149
|
+
child.stderr.on("data", (chunk) => {
|
|
150
|
+
stderr += chunk;
|
|
151
|
+
});
|
|
152
|
+
child.stdin.on("error", () => {});
|
|
153
|
+
|
|
154
|
+
if (child.pid) {
|
|
155
|
+
onSpawn?.(child.pid);
|
|
156
|
+
}
|
|
157
|
+
child.stdin.end(prompt);
|
|
158
|
+
if (timeoutMs && child.pid) {
|
|
159
|
+
timer = setTimeout(() => {
|
|
160
|
+
errorMessage = `dsh did not finish within ${Math.round(timeoutMs / 1000)}s and was stopped.`;
|
|
161
|
+
try {
|
|
162
|
+
process.kill(-child.pid, "SIGTERM");
|
|
163
|
+
} catch {
|
|
164
|
+
// Already gone.
|
|
165
|
+
}
|
|
166
|
+
}, timeoutMs);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
child.on("close", (code, signal) => {
|
|
170
|
+
const tail = parseEventLine(buffer);
|
|
171
|
+
if (tail) {
|
|
172
|
+
handleEvent(tail);
|
|
173
|
+
}
|
|
174
|
+
finish(() => {
|
|
175
|
+
if (!errorMessage && code !== 0) {
|
|
176
|
+
errorMessage = stderr.trim() || (signal ? `dsh exited with signal ${signal}` : `dsh exited with code ${code}`);
|
|
177
|
+
}
|
|
178
|
+
const exitStatus = errorMessage ? 1 : (code ?? 1);
|
|
179
|
+
const rendered = renderRunResult({
|
|
180
|
+
title: "dsh",
|
|
181
|
+
finalText,
|
|
182
|
+
errorMessage,
|
|
183
|
+
dshSessionId,
|
|
184
|
+
model: resolvedModel,
|
|
185
|
+
permissionMode
|
|
186
|
+
});
|
|
187
|
+
resolve({
|
|
188
|
+
exitStatus,
|
|
189
|
+
payload: { finalText, errorMessage, usage },
|
|
190
|
+
rendered,
|
|
191
|
+
summary: "",
|
|
192
|
+
dshSessionId,
|
|
193
|
+
model: resolvedModel,
|
|
194
|
+
permissionMode,
|
|
195
|
+
cwd
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function compareSemver(left, right) {
|
|
203
|
+
const parse = (value) => {
|
|
204
|
+
const [core, pre = ""] = value.split("-");
|
|
205
|
+
return { nums: core.split(".").map(Number), pre };
|
|
206
|
+
};
|
|
207
|
+
const a = parse(left);
|
|
208
|
+
const b = parse(right);
|
|
209
|
+
for (let i = 0; i < 3; i += 1) {
|
|
210
|
+
if (a.nums[i] !== b.nums[i]) {
|
|
211
|
+
return a.nums[i] < b.nums[i] ? -1 : 1;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (a.pre === b.pre) {
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
if (!a.pre) {
|
|
218
|
+
return 1;
|
|
219
|
+
}
|
|
220
|
+
if (!b.pre) {
|
|
221
|
+
return -1;
|
|
222
|
+
}
|
|
223
|
+
return a.pre < b.pre ? -1 : 1;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function isSupportedDshVersion(version) {
|
|
227
|
+
return Boolean(version) && compareSemver(version, "0.2.0-rc.2") >= 0 && compareSemver(version, "0.3.0") < 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function getDshAvailability(cwd) {
|
|
231
|
+
const result = binaryAvailable("dsh", ["--version"], { cwd });
|
|
232
|
+
if (!result.available) {
|
|
233
|
+
return { available: false, detail: result.detail, version: null, supported: false };
|
|
234
|
+
}
|
|
235
|
+
const match = result.detail.match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?/);
|
|
236
|
+
const version = match ? match[0] : null;
|
|
237
|
+
return { available: true, detail: result.detail, version, supported: isSupportedDshVersion(version) };
|
|
238
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export function ensureAbsolutePath(cwd, maybePath) {
|
|
6
|
+
return path.isAbsolute(maybePath) ? maybePath : path.resolve(cwd, maybePath);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createTempDir(prefix = "dsh-plugin-") {
|
|
10
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function readJsonFile(filePath) {
|
|
14
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function writeJsonFile(filePath, value) {
|
|
18
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function safeReadFile(filePath) {
|
|
22
|
+
return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isProbablyText(buffer) {
|
|
26
|
+
const sample = buffer.subarray(0, Math.min(buffer.length, 4096));
|
|
27
|
+
for (const value of sample) {
|
|
28
|
+
if (value === 0) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function readStdinIfPiped() {
|
|
36
|
+
if (process.stdin.isTTY) {
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
return fs.readFileSync(0, "utf8");
|
|
40
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { runCommand } from "./process.mjs";
|
|
2
|
+
|
|
3
|
+
// Git is directly executable on Windows. Repository-derived arguments must never pass through a shell.
|
|
4
|
+
export function ensureGitRepository(cwd) {
|
|
5
|
+
const result = runCommand("git", ["rev-parse", "--show-toplevel"], { cwd, shell: false });
|
|
6
|
+
const errorCode = result.error && "code" in result.error ? result.error.code : null;
|
|
7
|
+
if (errorCode === "ENOENT") {
|
|
8
|
+
throw new Error("git is not installed. Install Git and retry.");
|
|
9
|
+
}
|
|
10
|
+
if (result.status !== 0) {
|
|
11
|
+
throw new Error("This command must run inside a Git repository.");
|
|
12
|
+
}
|
|
13
|
+
return result.stdout.trim();
|
|
14
|
+
}
|