@hasna/hooks 0.10.8 → 0.11.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/README.md +48 -0
- package/bin/hooks-mcp.js +352 -182
- package/bin/index.js +2314 -1607
- package/bin/native-readiness-entry.js +355 -0
- package/bin/native-safety-entry.js +13 -4
- package/dist/index.js +502 -284
- package/dist/lib/codex-safety-check.d.ts +2 -1
- package/dist/lib/codex-safety-trust.d.ts +40 -8
- package/dist/lib/installer.d.ts +11 -0
- package/dist/lib/native-readiness-command.d.ts +14 -0
- package/dist/lib/native-safety-registration.d.ts +38 -7
- package/dist/lib/readiness-process.d.ts +20 -0
- package/dist/native-readiness.d.ts +68 -0
- package/dist/native-readiness.js +328 -0
- package/hooks/hook-workspace-repos-guard/README.md +5 -2
- package/hooks/hook-workspace-repos-guard/src/hook.ts +13 -4
- package/hooks/native-readiness-entry.ts +31 -0
- package/package.json +7 -2
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
// src/native-readiness.ts
|
|
2
|
+
var import_node_crypto = require("node:crypto");
|
|
3
|
+
|
|
4
|
+
// src/lib/readiness-process.ts
|
|
5
|
+
var import_node_child_process = require("node:child_process");
|
|
6
|
+
var supervisor = String.raw`
|
|
7
|
+
const {spawn}=require("node:child_process");
|
|
8
|
+
const spec=JSON.parse(process.argv.at(-1));
|
|
9
|
+
// This runtime was created detached and is its group's live leader. Its
|
|
10
|
+
// independent timer and parent pipe remain effective if the observer exits
|
|
11
|
+
// or its event loop stops responding. Only this freshly created tree is hit.
|
|
12
|
+
const cleanup=()=>{try{process.kill(-process.pid,"SIGKILL")}catch{process.exit(1)}};
|
|
13
|
+
setTimeout(cleanup,spec.timeout);
|
|
14
|
+
process.stdin.once("end",cleanup);
|
|
15
|
+
process.stdin.once("error",cleanup);
|
|
16
|
+
process.stdin.on("data",cleanup);
|
|
17
|
+
process.stdin.resume();
|
|
18
|
+
for(const signal of ["SIGTERM","SIGINT","SIGHUP"])process.once(signal,cleanup);
|
|
19
|
+
let done=false, out=Buffer.alloc(0), err=Buffer.alloc(0);
|
|
20
|
+
function report(status,error){
|
|
21
|
+
if(done)return; done=true;
|
|
22
|
+
process.stdout.write(JSON.stringify({status,error,stdout:out.toString("utf8"),stderr:err.toString("utf8")})+"\n");
|
|
23
|
+
}
|
|
24
|
+
const child=spawn(spec.command,spec.args,{stdio:["ignore","pipe","pipe"],env:process.env});
|
|
25
|
+
for(const [stream,name] of [[child.stdout,"stdout"],[child.stderr,"stderr"]]){
|
|
26
|
+
stream.on("data",bytes=>{
|
|
27
|
+
if(done)return;
|
|
28
|
+
const before=name==="stdout"?out:err;
|
|
29
|
+
if(before.length+bytes.length>spec.maxBuffer){report(null,{code:"ENOBUFS"});return}
|
|
30
|
+
if(name==="stdout")out=Buffer.concat([out,bytes]);else err=Buffer.concat([err,bytes]);
|
|
31
|
+
});
|
|
32
|
+
stream.on("error",()=>report(null,{code:"ECHILD"}));
|
|
33
|
+
}
|
|
34
|
+
child.on("error",()=>report(null,{code:"ECHILD"}));
|
|
35
|
+
// A successful observation includes complete streams. A nested client that
|
|
36
|
+
// keeps them open after its parent exits is a failure requiring group cleanup.
|
|
37
|
+
child.on("exit",()=>setTimeout(()=>report(null,{code:"ECHILD"}),100));
|
|
38
|
+
child.on("close",(status,signal)=>report(status,signal?{code:"ECHILD"}:undefined));
|
|
39
|
+
`;
|
|
40
|
+
function runReadinessProcess(command, args, options) {
|
|
41
|
+
if (!["darwin", "linux"].includes(process.platform)) {
|
|
42
|
+
return Promise.resolve({ status: null, error: { code: "ENOTSUP" }, stdout: "", stderr: "" });
|
|
43
|
+
}
|
|
44
|
+
if (options.signal?.aborted) {
|
|
45
|
+
return Promise.resolve({ status: null, error: { code: "ABORT_ERR" }, stdout: "", stderr: "" });
|
|
46
|
+
}
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
const child = import_node_child_process.spawn(command, ["--config=/dev/null", "--no-env-file", "--no-macros", "--no-install", "--eval", supervisor, "--", JSON.stringify({ command, args, maxBuffer: options.maxBuffer, timeout: options.timeout })], {
|
|
49
|
+
cwd: options.cwd,
|
|
50
|
+
env: options.env,
|
|
51
|
+
detached: true,
|
|
52
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
53
|
+
});
|
|
54
|
+
let status = null, error;
|
|
55
|
+
let stopping = false, exited = false, finished = false;
|
|
56
|
+
let stdout = "", stderr = "", control = "";
|
|
57
|
+
const maximumEnvelope = options.maxBuffer * 12 + 1024;
|
|
58
|
+
let cleanup;
|
|
59
|
+
const finish = () => {
|
|
60
|
+
if (finished)
|
|
61
|
+
return;
|
|
62
|
+
finished = true;
|
|
63
|
+
clearTimeout(deadline);
|
|
64
|
+
clearTimeout(cleanup);
|
|
65
|
+
options.signal?.removeEventListener("abort", abort);
|
|
66
|
+
child.stdin?.destroy();
|
|
67
|
+
child.stdout?.destroy();
|
|
68
|
+
child.stderr?.destroy();
|
|
69
|
+
resolve({ status, error, stdout, stderr });
|
|
70
|
+
};
|
|
71
|
+
const stop = (code) => {
|
|
72
|
+
if (code && !finished)
|
|
73
|
+
error ??= { code };
|
|
74
|
+
if (finished || stopping)
|
|
75
|
+
return;
|
|
76
|
+
stopping = true;
|
|
77
|
+
if (exited || !child.pid) {
|
|
78
|
+
error ??= { code: "ECHILD" };
|
|
79
|
+
} else {
|
|
80
|
+
try {
|
|
81
|
+
child.stdin.end();
|
|
82
|
+
} catch {
|
|
83
|
+
error = { code: "ECHILD" };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
cleanup = setTimeout(() => {
|
|
87
|
+
error = { code: "ECHILD" };
|
|
88
|
+
finish();
|
|
89
|
+
}, 1000);
|
|
90
|
+
};
|
|
91
|
+
const abort = () => stop("ABORT_ERR");
|
|
92
|
+
const deadline = setTimeout(() => stop("ETIMEDOUT"), options.timeout);
|
|
93
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
94
|
+
if (options.signal?.aborted)
|
|
95
|
+
abort();
|
|
96
|
+
child.stdin?.on("error", () => {
|
|
97
|
+
if (!stopping)
|
|
98
|
+
stop("ECHILD");
|
|
99
|
+
});
|
|
100
|
+
child.stdout?.on("data", (bytes) => {
|
|
101
|
+
if (stopping)
|
|
102
|
+
return;
|
|
103
|
+
control += bytes.toString("utf8");
|
|
104
|
+
if (Buffer.byteLength(control) > maximumEnvelope)
|
|
105
|
+
return stop("ENOBUFS");
|
|
106
|
+
if (!control.endsWith(`
|
|
107
|
+
`))
|
|
108
|
+
return;
|
|
109
|
+
try {
|
|
110
|
+
const value = JSON.parse(control);
|
|
111
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !["status", "error", "stdout", "stderr"].includes(key)) || !(value.status === null || Number.isInteger(value.status) && value.status >= 0 && value.status <= 255) || typeof value.stdout !== "string" || typeof value.stderr !== "string" || Buffer.byteLength(value.stdout) > options.maxBuffer || Buffer.byteLength(value.stderr) > options.maxBuffer || value.error !== undefined && (Object.keys(value.error).join() !== "code" || !["ENOBUFS", "ECHILD"].includes(value.error.code))) {
|
|
112
|
+
return stop("EPROTO");
|
|
113
|
+
}
|
|
114
|
+
status = value.status;
|
|
115
|
+
stdout = value.stdout;
|
|
116
|
+
stderr = value.stderr;
|
|
117
|
+
stop(value.error?.code);
|
|
118
|
+
} catch {
|
|
119
|
+
stop("EPROTO");
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
child.stdout?.on("error", () => stop("EPROTO"));
|
|
123
|
+
child.stderr?.on("data", () => stop("ECHILD"));
|
|
124
|
+
child.stderr?.on("error", () => stop("ECHILD"));
|
|
125
|
+
child.on("error", () => {
|
|
126
|
+
error = { code: "ECHILD" };
|
|
127
|
+
stop();
|
|
128
|
+
});
|
|
129
|
+
child.on("exit", () => {
|
|
130
|
+
exited = true;
|
|
131
|
+
if (!stopping)
|
|
132
|
+
stop("ECHILD");
|
|
133
|
+
});
|
|
134
|
+
child.on("close", () => {
|
|
135
|
+
if (!stopping)
|
|
136
|
+
error = { code: "ECHILD" };
|
|
137
|
+
finish();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/native-readiness.ts
|
|
143
|
+
var import_node_fs = require("node:fs");
|
|
144
|
+
var import_node_os = require("node:os");
|
|
145
|
+
var import_node_path = require("node:path");
|
|
146
|
+
|
|
147
|
+
class ReadinessError extends Error {
|
|
148
|
+
code;
|
|
149
|
+
constructor(code) {
|
|
150
|
+
super(code);
|
|
151
|
+
this.code = code;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function need(value, code) {
|
|
155
|
+
if (!value)
|
|
156
|
+
throw new ReadinessError(code);
|
|
157
|
+
}
|
|
158
|
+
var sha = (value) => import_node_crypto.createHash("sha256").update(value).digest("hex");
|
|
159
|
+
var digest = (v) => typeof v === "string" && /^[a-f0-9]{64}$/.test(v);
|
|
160
|
+
var absolute = (v) => typeof v === "string" && v.length <= 4096 && import_node_path.isAbsolute(v) && import_node_path.resolve(v) === v && !/[\0\r\n]/.test(v);
|
|
161
|
+
var object = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
162
|
+
var keys = (v, expected) => object(v) && Object.keys(v).sort().join() === expected.sort().join();
|
|
163
|
+
var pin = (v) => keys(v, ["path", "sha256"]) && absolute(v.path) && digest(v.sha256);
|
|
164
|
+
function isReadinessProvider(value) {
|
|
165
|
+
return value === "claude" || value === "codex" || value === "sumi";
|
|
166
|
+
}
|
|
167
|
+
function inside(home, path) {
|
|
168
|
+
const part = import_node_path.relative(home, path);
|
|
169
|
+
return part !== "" && part !== ".." && !part.startsWith("../") && !import_node_path.isAbsolute(part);
|
|
170
|
+
}
|
|
171
|
+
function parseTrashReadinessBinding(input, provider, home) {
|
|
172
|
+
const b = input;
|
|
173
|
+
need(isReadinessProvider(provider) && absolute(home), "binding_invalid");
|
|
174
|
+
need(keys(b, ["schema", "provider", "home", "runtime", "trash", "plan", "timeoutMs"]) && b.schema === "hasna.hooks.trash-readiness/v1" && b.provider === provider && b.home === home && pin(b.runtime) && pin(b.plan) && keys(b.trash, ["root", "version", "cliSha256"]) && absolute(b.trash.root) && typeof b.trash.version === "string" && b.trash.version.length <= 128 && /^\d+\.\d+\.\d+$/.test(b.trash.version) && digest(b.trash.cliSha256) && Number.isInteger(b.timeoutMs) && b.timeoutMs >= 1000 && b.timeoutMs <= 15000 && inside(home, b.plan.path) && inside(home, b.trash.root), "binding_invalid");
|
|
175
|
+
return structuredClone(b);
|
|
176
|
+
}
|
|
177
|
+
function trashReadinessBindingPath(provider, home = import_node_os.homedir()) {
|
|
178
|
+
need(isReadinessProvider(provider) && absolute(home), "binding_invalid");
|
|
179
|
+
return import_node_path.join(home, ".hasna", "hooks", "native", "trash-readiness", provider + ".json");
|
|
180
|
+
}
|
|
181
|
+
function readSafe(path, maximum, executable = false) {
|
|
182
|
+
need(absolute(path) && import_node_fs.realpathSync(path) === path, "unsafe_path");
|
|
183
|
+
for (let parent = import_node_path.dirname(path);; parent = import_node_path.dirname(parent)) {
|
|
184
|
+
const stat = import_node_fs.lstatSync(parent);
|
|
185
|
+
need(stat.isDirectory() && [0, process.getuid?.()].includes(stat.uid) && !(stat.mode & 18), "unsafe_path");
|
|
186
|
+
if (parent === import_node_path.dirname(parent))
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
const fd = import_node_fs.openSync(path, import_node_fs.constants.O_RDONLY | import_node_fs.constants.O_NOFOLLOW | import_node_fs.constants.O_NONBLOCK);
|
|
190
|
+
try {
|
|
191
|
+
const stat = import_node_fs.fstatSync(fd);
|
|
192
|
+
need(stat.isFile() && [0, process.getuid?.()].includes(stat.uid) && !(stat.mode & 18) && stat.size <= maximum && (!executable || !!(stat.mode & 64)), "unsafe_path");
|
|
193
|
+
const bytes = Buffer.alloc(stat.size + 1);
|
|
194
|
+
let length = 0;
|
|
195
|
+
while (length < bytes.length) {
|
|
196
|
+
const count = import_node_fs.readSync(fd, bytes, length, bytes.length - length, null);
|
|
197
|
+
if (!count)
|
|
198
|
+
break;
|
|
199
|
+
length += count;
|
|
200
|
+
}
|
|
201
|
+
const after = import_node_fs.fstatSync(fd);
|
|
202
|
+
need(length === stat.size && after.size === stat.size && after.mtimeMs === stat.mtimeMs && after.ctimeMs === stat.ctimeMs, "file_changed");
|
|
203
|
+
return bytes.subarray(0, length);
|
|
204
|
+
} finally {
|
|
205
|
+
import_node_fs.closeSync(fd);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
var safeFailureCodes = new Set([
|
|
209
|
+
"unsupported_platform",
|
|
210
|
+
"station_detection_failed",
|
|
211
|
+
"station_mismatch",
|
|
212
|
+
"hosted_unavailable",
|
|
213
|
+
"authentication_refused",
|
|
214
|
+
"station_setup_required",
|
|
215
|
+
"invalid_status",
|
|
216
|
+
"deadline_exceeded",
|
|
217
|
+
"retention_mismatch",
|
|
218
|
+
"managed_instructions_refused",
|
|
219
|
+
"removal_instruction_missing",
|
|
220
|
+
"instructions_changed",
|
|
221
|
+
"native_config_changed",
|
|
222
|
+
"guard_refused",
|
|
223
|
+
"owner_refused"
|
|
224
|
+
]);
|
|
225
|
+
async function runTrashReadiness(provider, d = {}) {
|
|
226
|
+
const started = performance.now();
|
|
227
|
+
const result = (code, extra = {}) => ({
|
|
228
|
+
schema: "hasna.hooks.trash-readiness-result/v1",
|
|
229
|
+
provider,
|
|
230
|
+
ready: code === "ready",
|
|
231
|
+
code,
|
|
232
|
+
nativeAdoptionVerified: false,
|
|
233
|
+
...extra
|
|
234
|
+
});
|
|
235
|
+
let phase = "binding";
|
|
236
|
+
try {
|
|
237
|
+
const home = d.home ?? import_node_fs.realpathSync(import_node_os.homedir()), read = d.read ?? readSafe;
|
|
238
|
+
need(isReadinessProvider(provider), "binding_invalid");
|
|
239
|
+
const bindingPath = trashReadinessBindingPath(provider, home), bindingBytes = read(bindingPath, 65536);
|
|
240
|
+
const b = parseTrashReadinessBinding(JSON.parse(bindingBytes.toString("utf8")), provider, home);
|
|
241
|
+
phase = "consumer";
|
|
242
|
+
const checkPin = (p, maximum, executable = false) => {
|
|
243
|
+
const bytes = read(p.path, maximum, executable);
|
|
244
|
+
need(sha(bytes) === p.sha256, "file_changed");
|
|
245
|
+
return bytes;
|
|
246
|
+
};
|
|
247
|
+
checkPin(b.runtime, 200000000, true);
|
|
248
|
+
const manifestPath = import_node_path.join(b.trash.root, "package.json"), manifestBytes = read(manifestPath, 65536);
|
|
249
|
+
const manifest = JSON.parse(manifestBytes.toString("utf8"));
|
|
250
|
+
need(manifest.name === "@hasna/trash" && manifest.version === b.trash.version && manifest.bin?.trash === "dist/cli/index.js", "package_mismatch");
|
|
251
|
+
const cli = import_node_path.join(b.trash.root, "dist", "cli", "index.js");
|
|
252
|
+
checkPin({ path: cli, sha256: b.trash.cliSha256 }, 64 * 1024 * 1024, true);
|
|
253
|
+
const plan = JSON.parse(checkPin(b.plan, 65536).toString("utf8"));
|
|
254
|
+
need(plan?.schema === "hasna.trash.station-setup-plan/v1" && plan.stage === "ready" && plan.configurationReady === true && plan.nativeAdoptionVerified === false && digest(plan.planDigest) && plan.spec?.provider === provider && plan.spec.home === home && typeof plan.spec.station === "string" && /^[a-z0-9][a-z0-9-]{0,62}$/.test(plan.spec.station) && plan.spec.runtime?.path === b.runtime.path && plan.spec.runtime?.sha256 === b.runtime.sha256 && absolute(plan.spec.cwd) && (plan.spec.cwd === home || inside(home, plan.spec.cwd)) && typeof plan.spec.path === "string" && plan.spec.path.length <= 8192 && plan.spec.path.split(":").every(absolute), "plan_invalid");
|
|
255
|
+
const args = ["--config=/dev/null", "--no-env-file", "--no-macros", "--no-install", cli, "station", "check", "--plan", b.plan.path];
|
|
256
|
+
const remaining = () => Math.floor(b.timeoutMs - (performance.now() - started));
|
|
257
|
+
const timeout = remaining();
|
|
258
|
+
need(timeout > 0, "deadline_exceeded");
|
|
259
|
+
const child = await (d.spawn ?? runReadinessProcess)(b.runtime.path, args, {
|
|
260
|
+
cwd: home,
|
|
261
|
+
env: { HOME: home, PATH: plan.spec.path, LANG: "C", NO_COLOR: "1" },
|
|
262
|
+
timeout,
|
|
263
|
+
maxBuffer: 16384,
|
|
264
|
+
signal: d.signal
|
|
265
|
+
});
|
|
266
|
+
need(remaining() > 0, "deadline_exceeded");
|
|
267
|
+
if (child.error && "code" in child.error && child.error.code === "ETIMEDOUT")
|
|
268
|
+
return result("deadline_exceeded");
|
|
269
|
+
if (child.error || child.signal || child.status === null)
|
|
270
|
+
return result("execution_failed");
|
|
271
|
+
need(typeof child.stdout === "string" && typeof child.stderr === "string" && Buffer.byteLength(child.stdout) <= 16384 && Buffer.byteLength(child.stderr) <= 16384, "output_invalid");
|
|
272
|
+
let value;
|
|
273
|
+
try {
|
|
274
|
+
value = JSON.parse(child.stdout);
|
|
275
|
+
} catch {
|
|
276
|
+
return result("output_invalid");
|
|
277
|
+
}
|
|
278
|
+
if (child.status !== 0 || value?.configurationReady !== true) {
|
|
279
|
+
return result(safeFailureCodes.has(value?.code) ? value.code : "check_failed");
|
|
280
|
+
}
|
|
281
|
+
need(value.schema === "hasna.trash.station-setup-result/v1" && value.station === plan.spec.station && value.provider === provider && value.planDigest === plan.planDigest && value.serviceReady === true && value.guardVerified === true && value.managedInstructionsVerified === true && value.mcpConfigurationVerified === true && value.mcpExecutionVerified === false && value.nativeAdoptionVerified === false && child.stderr.length === 0, "output_invalid");
|
|
282
|
+
checkPin(b.plan, 65536);
|
|
283
|
+
checkPin({ path: cli, sha256: b.trash.cliSha256 }, 64 * 1024 * 1024, true);
|
|
284
|
+
checkPin(b.runtime, 200000000, true);
|
|
285
|
+
need(read(bindingPath, 65536).equals(bindingBytes) && read(manifestPath, 65536).equals(manifestBytes), "file_changed");
|
|
286
|
+
need(remaining() > 0, "deadline_exceeded");
|
|
287
|
+
return result("ready", { station: value.station, planDigest: value.planDigest });
|
|
288
|
+
} catch (error) {
|
|
289
|
+
return result(error instanceof ReadinessError ? error.code : phase === "binding" && error?.code === "ENOENT" ? "binding_missing" : "binding_invalid");
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function readinessContext(result) {
|
|
293
|
+
return result.ready ? `Trash readiness verified for ${result.station} (${result.provider}). Use Trash for recoverable file removal.` : `Trash readiness needs attention (${result.provider}: ${result.code}). Preserve files; do not fall back to direct deletion. Refresh the station setup before removal.`;
|
|
294
|
+
}
|
|
295
|
+
function readinessSessionStartOutput(result) {
|
|
296
|
+
return { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: readinessContext(result) } };
|
|
297
|
+
}
|
|
298
|
+
async function handleReadinessSessionStart(provider, event, dependencies = {}) {
|
|
299
|
+
const input = event;
|
|
300
|
+
if (!object(input) || input.hook_event_name !== "SessionStart" || !["startup", "resume", "clear", "fork", "compact"].includes(input.source)) {
|
|
301
|
+
return readinessSessionStartOutput({ schema: "hasna.hooks.trash-readiness-result/v1", provider, ready: false, code: "input_invalid", nativeAdoptionVerified: false });
|
|
302
|
+
}
|
|
303
|
+
if (input.source === "compact")
|
|
304
|
+
return {};
|
|
305
|
+
return readinessSessionStartOutput(await runTrashReadiness(provider, dependencies));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// hooks/native-readiness-entry.ts
|
|
309
|
+
async function main() {
|
|
310
|
+
const provider = process.argv[2];
|
|
311
|
+
if (!isReadinessProvider(provider)) {
|
|
312
|
+
process.stderr.write(`Trash readiness needs attention (unsupported provider).
|
|
313
|
+
`);
|
|
314
|
+
process.exitCode = 1;
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const invalid = () => readinessSessionStartOutput({
|
|
318
|
+
schema: "hasna.hooks.trash-readiness-result/v1",
|
|
319
|
+
provider,
|
|
320
|
+
ready: false,
|
|
321
|
+
code: "input_invalid",
|
|
322
|
+
nativeAdoptionVerified: false
|
|
323
|
+
});
|
|
324
|
+
try {
|
|
325
|
+
const input = await new Promise((accept, reject) => {
|
|
326
|
+
const chunks = [];
|
|
327
|
+
let size = 0;
|
|
328
|
+
const finish = (error) => {
|
|
329
|
+
clearTimeout(timer);
|
|
330
|
+
process.stdin.pause();
|
|
331
|
+
process.stdin.removeAllListeners("data");
|
|
332
|
+
process.stdin.removeAllListeners("end");
|
|
333
|
+
process.stdin.removeAllListeners("error");
|
|
334
|
+
error ? reject(error) : accept(Buffer.concat(chunks));
|
|
335
|
+
};
|
|
336
|
+
const timer = setTimeout(() => finish(new Error), 1000);
|
|
337
|
+
process.stdin.on("data", (chunk) => {
|
|
338
|
+
const bytes = Buffer.from(chunk);
|
|
339
|
+
size += bytes.length;
|
|
340
|
+
if (size > 65536)
|
|
341
|
+
finish(new Error);
|
|
342
|
+
else
|
|
343
|
+
chunks.push(bytes);
|
|
344
|
+
});
|
|
345
|
+
process.stdin.once("end", () => finish());
|
|
346
|
+
process.stdin.once("error", () => finish(new Error));
|
|
347
|
+
});
|
|
348
|
+
process.stdout.write(JSON.stringify(await handleReadinessSessionStart(provider, JSON.parse(input.toString("utf8")))) + `
|
|
349
|
+
`);
|
|
350
|
+
} catch {
|
|
351
|
+
process.stdout.write(JSON.stringify(invalid()) + `
|
|
352
|
+
`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
main();
|
|
@@ -1303,7 +1303,7 @@ function segmentOperation(segment) {
|
|
|
1303
1303
|
const trimmed = segment.trim();
|
|
1304
1304
|
if (!trimmed)
|
|
1305
1305
|
return "read";
|
|
1306
|
-
if (/(?:^|\s)(?:rm|rmdir|unlink|shred|trash|rmtree|del)(?:\s|$)/.test(trimmed))
|
|
1306
|
+
if (/(?:^|\s)\\?(?:[^\s;&|<>()]*\/)?(?:rm|rmdir|unlink|shred|trash|rmtree|del)(?:\s|$)/.test(trimmed))
|
|
1307
1307
|
return "delete";
|
|
1308
1308
|
if (/\bgit\b/.test(trimmed)) {
|
|
1309
1309
|
if (/\bgit\b[^;&|]*\b(?:clean|rm)\b/.test(trimmed))
|
|
@@ -1312,7 +1312,7 @@ function segmentOperation(segment) {
|
|
|
1312
1312
|
return "write";
|
|
1313
1313
|
return "read";
|
|
1314
1314
|
}
|
|
1315
|
-
if (/(?:^|\s)(?:mkdir|mkfile|touch|mv|cp|ln|tee|install|dd)(?:\s|$)/.test(trimmed))
|
|
1315
|
+
if (/(?:^|\s)\\?(?:[^\s;&|<>()]*\/)?(?:mkdir|mkfile|touch|mv|cp|ln|tee|install|dd)(?:\s|$)/.test(trimmed))
|
|
1316
1316
|
return "write";
|
|
1317
1317
|
if (/\b(?:curl|wget)\b/.test(trimmed)) {
|
|
1318
1318
|
if (WRITE_FLAGS.test(trimmed))
|
|
@@ -1455,8 +1455,17 @@ function bashTargets(command, home, cwd) {
|
|
|
1455
1455
|
if (foundExplicit || stage.op === "read")
|
|
1456
1456
|
continue;
|
|
1457
1457
|
const words = shellWords(stage.commandPart);
|
|
1458
|
+
let operandStart = 1;
|
|
1459
|
+
const tool = (words[0]?.text ?? "").split("/").pop();
|
|
1460
|
+
if (tool === "trash" && ["put", "rm", "delete", "remove"].includes(words[1]?.text ?? ""))
|
|
1461
|
+
operandStart = 2;
|
|
1462
|
+
if (tool === "git") {
|
|
1463
|
+
const sub = words.findIndex((word, index2) => index2 > 0 && (word.text === "rm" || word.text === "clean"));
|
|
1464
|
+
if (sub > 0)
|
|
1465
|
+
operandStart = sub + 1;
|
|
1466
|
+
}
|
|
1458
1467
|
let endOfOptions = false;
|
|
1459
|
-
const nonFlag = words.slice(
|
|
1468
|
+
const nonFlag = words.slice(operandStart).flatMap((word) => {
|
|
1460
1469
|
if (!endOfOptions && word.text === "--") {
|
|
1461
1470
|
endOfOptions = true;
|
|
1462
1471
|
return [];
|
|
@@ -1466,7 +1475,7 @@ function bashTargets(command, home, cwd) {
|
|
|
1466
1475
|
endOfOptions = true;
|
|
1467
1476
|
return [word.opaque ? "." : word.text];
|
|
1468
1477
|
});
|
|
1469
|
-
const everyOperand = stage.op === "delete" || /(?:^|\s)(?:mkdir|mkfile|touch|tee|truncate|install|mv)(?:\s|$)/.test(stage.commandPart) || /(?:^|\s)sed(?:\s|$)/.test(stage.commandPart) && /(?:^|\s)(?:-i\S*|--in-place\S*)(?:\s|$)/.test(stage.commandPart);
|
|
1478
|
+
const everyOperand = stage.op === "delete" || /(?:^|\s)\\?(?:[^\s;&|<>()]*\/)?(?:mkdir|mkfile|touch|tee|truncate|install|mv)(?:\s|$)/.test(stage.commandPart) || /(?:^|\s)sed(?:\s|$)/.test(stage.commandPart) && /(?:^|\s)(?:-i\S*|--in-place\S*)(?:\s|$)/.test(stage.commandPart);
|
|
1470
1479
|
const operandTargets = [];
|
|
1471
1480
|
if (everyOperand) {
|
|
1472
1481
|
for (const operand of nonFlag.length > 0 ? nonFlag : ["."])
|