dsh-hashline-edit 0.1.0 → 0.1.1
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 +3 -2
- package/lib/index.js +205 -149
- package/lib/types/dsh/shell-view.d.ts +13 -3
- package/lib/types/src/bash-view-banner.d.ts +20 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -130,11 +130,12 @@ The plugin installs a `tools/post-execute` listener for the host's `pwsh` and `b
|
|
|
130
130
|
| Command shape | Served as |
|
|
131
131
|
| --- | --- |
|
|
132
132
|
| `Get-Content <file>`, `type <file>`, `cat <file>` | the file's lines |
|
|
133
|
-
| `Get-Content <file> \| Select-Object -First
|
|
133
|
+
| `Get-Content <file> \| Select-Object -First\|-Last\|-Skip <N>` | the bounded slice |
|
|
134
|
+
| `Get-Content <file> \| Select-Object -Skip N -First M` (either order) | disk lines N+1 through N+M |
|
|
134
135
|
| `Select-String <pattern> <file>` | the matched lines, with their line numbers |
|
|
135
136
|
| `rg <pattern> <file>` (explicit single file, no switches) | the matched lines, with their line numbers |
|
|
136
137
|
|
|
137
|
-
Alignment is strict: stdout lines are mapped to 1-based disk line numbers with a monotonic, unique match (search output's own line numbers are used as hints). Anything the view cannot prove — an unknown pipeline, a
|
|
138
|
+
Alignment is strict: stdout lines are mapped to 1-based disk line numbers with a monotonic, unique match (search output's own line numbers are used as hints). Every served view is prefixed with the shared `[Bash view: <file> (lines a-b of N total)]` page header, so the positions the raw output used to name are not lost. Anything the view cannot prove — an unknown pipeline, a statement separator (`;`), a variable path (`$f`), a wildcard, formatted output, a truncated capture, a failed command — passes through byte-for-byte. The hook never guesses and never serves the wrong occurrence of a duplicated line.
|
|
138
139
|
|
|
139
140
|
## Encoding governance
|
|
140
141
|
|
package/lib/index.js
CHANGED
|
@@ -2268,8 +2268,156 @@ ${escalationHintMarker("operation")}`,
|
|
|
2268
2268
|
}
|
|
2269
2269
|
};
|
|
2270
2270
|
|
|
2271
|
+
// src/bash-view-banner.ts
|
|
2272
|
+
var BASH_VIEW_LABEL = "Bash view";
|
|
2273
|
+
|
|
2274
|
+
// dsh/host-compat.ts
|
|
2275
|
+
import { realpath } from "node:fs/promises";
|
|
2276
|
+
import { resolve as resolve3 } from "node:path";
|
|
2277
|
+
var DEFAULT_MAX_LINES = 2e3;
|
|
2278
|
+
var DEFAULT_MAX_BYTES = 50 * 1024;
|
|
2279
|
+
function splitLinesForCounting(content) {
|
|
2280
|
+
if (content.length === 0) return [];
|
|
2281
|
+
const lines = content.split("\n");
|
|
2282
|
+
if (content.endsWith("\n")) lines.pop();
|
|
2283
|
+
return lines;
|
|
2284
|
+
}
|
|
2285
|
+
function formatSize(bytes) {
|
|
2286
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
2287
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
2288
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
2289
|
+
}
|
|
2290
|
+
function truncateHead(content, options = {}) {
|
|
2291
|
+
const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
|
|
2292
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
2293
|
+
const totalBytes = Buffer.byteLength(content, "utf-8");
|
|
2294
|
+
const lines = splitLinesForCounting(content);
|
|
2295
|
+
const totalLines = lines.length;
|
|
2296
|
+
if (totalLines <= maxLines && totalBytes <= maxBytes) {
|
|
2297
|
+
return {
|
|
2298
|
+
content,
|
|
2299
|
+
truncated: false,
|
|
2300
|
+
truncatedBy: null,
|
|
2301
|
+
totalLines,
|
|
2302
|
+
totalBytes,
|
|
2303
|
+
outputLines: totalLines,
|
|
2304
|
+
outputBytes: totalBytes,
|
|
2305
|
+
lastLinePartial: false,
|
|
2306
|
+
firstLineExceedsLimit: false,
|
|
2307
|
+
maxLines,
|
|
2308
|
+
maxBytes
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
const firstLineBytes = Buffer.byteLength(lines[0] ?? "", "utf-8");
|
|
2312
|
+
if (firstLineBytes > maxBytes) {
|
|
2313
|
+
return {
|
|
2314
|
+
content: "",
|
|
2315
|
+
truncated: true,
|
|
2316
|
+
truncatedBy: "bytes",
|
|
2317
|
+
totalLines,
|
|
2318
|
+
totalBytes,
|
|
2319
|
+
outputLines: 0,
|
|
2320
|
+
outputBytes: 0,
|
|
2321
|
+
lastLinePartial: false,
|
|
2322
|
+
firstLineExceedsLimit: true,
|
|
2323
|
+
maxLines,
|
|
2324
|
+
maxBytes
|
|
2325
|
+
};
|
|
2326
|
+
}
|
|
2327
|
+
const outputLinesArr = [];
|
|
2328
|
+
let outputBytesCount = 0;
|
|
2329
|
+
let truncatedBy = "lines";
|
|
2330
|
+
for (let index = 0; index < lines.length && index < maxLines; index += 1) {
|
|
2331
|
+
const line = lines[index] ?? "";
|
|
2332
|
+
const lineBytes = Buffer.byteLength(line, "utf-8") + (index > 0 ? 1 : 0);
|
|
2333
|
+
if (outputBytesCount + lineBytes > maxBytes) {
|
|
2334
|
+
truncatedBy = "bytes";
|
|
2335
|
+
break;
|
|
2336
|
+
}
|
|
2337
|
+
outputLinesArr.push(line);
|
|
2338
|
+
outputBytesCount += lineBytes;
|
|
2339
|
+
}
|
|
2340
|
+
if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
|
|
2341
|
+
truncatedBy = "lines";
|
|
2342
|
+
}
|
|
2343
|
+
const outputContent = outputLinesArr.join("\n");
|
|
2344
|
+
return {
|
|
2345
|
+
content: outputContent,
|
|
2346
|
+
truncated: true,
|
|
2347
|
+
truncatedBy,
|
|
2348
|
+
totalLines,
|
|
2349
|
+
totalBytes,
|
|
2350
|
+
outputLines: outputLinesArr.length,
|
|
2351
|
+
outputBytes: Buffer.byteLength(outputContent, "utf-8"),
|
|
2352
|
+
lastLinePartial: false,
|
|
2353
|
+
firstLineExceedsLimit: false,
|
|
2354
|
+
maxLines,
|
|
2355
|
+
maxBytes
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
var fileMutationQueues = /* @__PURE__ */ new Map();
|
|
2359
|
+
var registrationQueue = Promise.resolve();
|
|
2360
|
+
function isMissingPathError(error) {
|
|
2361
|
+
return typeof error === "object" && error !== null && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
2362
|
+
}
|
|
2363
|
+
async function getMutationQueueKey(filePath) {
|
|
2364
|
+
const resolvedPath = resolve3(filePath);
|
|
2365
|
+
try {
|
|
2366
|
+
return await realpath(resolvedPath);
|
|
2367
|
+
} catch (error) {
|
|
2368
|
+
if (isMissingPathError(error)) return resolvedPath;
|
|
2369
|
+
throw error;
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
async function withFileMutationQueue(filePath, run) {
|
|
2373
|
+
const registration = registrationQueue.then(async () => {
|
|
2374
|
+
const key2 = await getMutationQueueKey(filePath);
|
|
2375
|
+
const currentQueue2 = fileMutationQueues.get(key2) ?? Promise.resolve();
|
|
2376
|
+
let releaseNext2 = () => {
|
|
2377
|
+
};
|
|
2378
|
+
const nextQueue = new Promise((resolveQueue) => {
|
|
2379
|
+
releaseNext2 = resolveQueue;
|
|
2380
|
+
});
|
|
2381
|
+
const chainedQueue2 = currentQueue2.then(() => nextQueue);
|
|
2382
|
+
fileMutationQueues.set(key2, chainedQueue2);
|
|
2383
|
+
return { key: key2, currentQueue: currentQueue2, chainedQueue: chainedQueue2, releaseNext: releaseNext2 };
|
|
2384
|
+
});
|
|
2385
|
+
registrationQueue = registration.then(
|
|
2386
|
+
() => void 0,
|
|
2387
|
+
() => void 0
|
|
2388
|
+
);
|
|
2389
|
+
const { key, currentQueue, chainedQueue, releaseNext } = await registration;
|
|
2390
|
+
await currentQueue;
|
|
2391
|
+
try {
|
|
2392
|
+
return await run();
|
|
2393
|
+
} finally {
|
|
2394
|
+
releaseNext();
|
|
2395
|
+
if (fileMutationQueues.get(key) === chainedQueue) fileMutationQueues.delete(key);
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
// src/file-content/served-header.ts
|
|
2400
|
+
function stopSuffix(stoppedAtBytes) {
|
|
2401
|
+
return stoppedAtBytes === void 0 ? "" : `; stopped at ${formatSize(stoppedAtBytes)}`;
|
|
2402
|
+
}
|
|
2403
|
+
function rangeOf(shown, totalLines) {
|
|
2404
|
+
if (shown === void 0 || shown.startLine === 1 && shown.endLine === totalLines) {
|
|
2405
|
+
return `${totalLines} lines total`;
|
|
2406
|
+
}
|
|
2407
|
+
return `lines ${shown.startLine}-${shown.endLine} of ${totalLines} total`;
|
|
2408
|
+
}
|
|
2409
|
+
function formatPageHeader(parts) {
|
|
2410
|
+
const count = rangeOf(parts.shownRange, parts.totalLines);
|
|
2411
|
+
const body = parts.verbatim === true ? `verbatim, ${count}` : count;
|
|
2412
|
+
const label = parts.label === void 0 ? "" : `${parts.label}: `;
|
|
2413
|
+
return `[${label}${parts.file} (${body}${stopSuffix(parts.stoppedAtBytes)})]`;
|
|
2414
|
+
}
|
|
2415
|
+
function formatSectionHeader(startLine, endLine, totalLines, stoppedAtBytes) {
|
|
2416
|
+
return `[Lines ${startLine}-${endLine} of ${totalLines} total${stopSuffix(stoppedAtBytes)}]`;
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2271
2419
|
// src/hashline/hash-identity.ts
|
|
2272
|
-
import { isAbsolute as isAbsolute3, resolve as
|
|
2420
|
+
import { isAbsolute as isAbsolute3, resolve as resolve4 } from "node:path";
|
|
2273
2421
|
|
|
2274
2422
|
// src/hashline/hasher.ts
|
|
2275
2423
|
import xxhash from "xxhash-wasm";
|
|
@@ -2408,7 +2556,7 @@ function canonicalAnchorPath(path) {
|
|
|
2408
2556
|
message: `Anchor derivation requires an absolute file path, got ${JSON.stringify(path)}.`
|
|
2409
2557
|
});
|
|
2410
2558
|
}
|
|
2411
|
-
return
|
|
2559
|
+
return resolve4(path);
|
|
2412
2560
|
}
|
|
2413
2561
|
function contentBaseIndex(canonText) {
|
|
2414
2562
|
return (xxh32(canonText) >>> 14) % HASH_SPACE;
|
|
@@ -6224,131 +6372,6 @@ async function anchorWalkFor(normalized, absolutePath, options) {
|
|
|
6224
6372
|
});
|
|
6225
6373
|
}
|
|
6226
6374
|
|
|
6227
|
-
// dsh/host-compat.ts
|
|
6228
|
-
import { realpath } from "node:fs/promises";
|
|
6229
|
-
import { resolve as resolve4 } from "node:path";
|
|
6230
|
-
var DEFAULT_MAX_LINES = 2e3;
|
|
6231
|
-
var DEFAULT_MAX_BYTES = 50 * 1024;
|
|
6232
|
-
function splitLinesForCounting(content) {
|
|
6233
|
-
if (content.length === 0) return [];
|
|
6234
|
-
const lines = content.split("\n");
|
|
6235
|
-
if (content.endsWith("\n")) lines.pop();
|
|
6236
|
-
return lines;
|
|
6237
|
-
}
|
|
6238
|
-
function formatSize(bytes) {
|
|
6239
|
-
if (bytes < 1024) return `${bytes}B`;
|
|
6240
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
6241
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
6242
|
-
}
|
|
6243
|
-
function truncateHead(content, options = {}) {
|
|
6244
|
-
const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
|
|
6245
|
-
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
6246
|
-
const totalBytes = Buffer.byteLength(content, "utf-8");
|
|
6247
|
-
const lines = splitLinesForCounting(content);
|
|
6248
|
-
const totalLines = lines.length;
|
|
6249
|
-
if (totalLines <= maxLines && totalBytes <= maxBytes) {
|
|
6250
|
-
return {
|
|
6251
|
-
content,
|
|
6252
|
-
truncated: false,
|
|
6253
|
-
truncatedBy: null,
|
|
6254
|
-
totalLines,
|
|
6255
|
-
totalBytes,
|
|
6256
|
-
outputLines: totalLines,
|
|
6257
|
-
outputBytes: totalBytes,
|
|
6258
|
-
lastLinePartial: false,
|
|
6259
|
-
firstLineExceedsLimit: false,
|
|
6260
|
-
maxLines,
|
|
6261
|
-
maxBytes
|
|
6262
|
-
};
|
|
6263
|
-
}
|
|
6264
|
-
const firstLineBytes = Buffer.byteLength(lines[0] ?? "", "utf-8");
|
|
6265
|
-
if (firstLineBytes > maxBytes) {
|
|
6266
|
-
return {
|
|
6267
|
-
content: "",
|
|
6268
|
-
truncated: true,
|
|
6269
|
-
truncatedBy: "bytes",
|
|
6270
|
-
totalLines,
|
|
6271
|
-
totalBytes,
|
|
6272
|
-
outputLines: 0,
|
|
6273
|
-
outputBytes: 0,
|
|
6274
|
-
lastLinePartial: false,
|
|
6275
|
-
firstLineExceedsLimit: true,
|
|
6276
|
-
maxLines,
|
|
6277
|
-
maxBytes
|
|
6278
|
-
};
|
|
6279
|
-
}
|
|
6280
|
-
const outputLinesArr = [];
|
|
6281
|
-
let outputBytesCount = 0;
|
|
6282
|
-
let truncatedBy = "lines";
|
|
6283
|
-
for (let index = 0; index < lines.length && index < maxLines; index += 1) {
|
|
6284
|
-
const line = lines[index] ?? "";
|
|
6285
|
-
const lineBytes = Buffer.byteLength(line, "utf-8") + (index > 0 ? 1 : 0);
|
|
6286
|
-
if (outputBytesCount + lineBytes > maxBytes) {
|
|
6287
|
-
truncatedBy = "bytes";
|
|
6288
|
-
break;
|
|
6289
|
-
}
|
|
6290
|
-
outputLinesArr.push(line);
|
|
6291
|
-
outputBytesCount += lineBytes;
|
|
6292
|
-
}
|
|
6293
|
-
if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
|
|
6294
|
-
truncatedBy = "lines";
|
|
6295
|
-
}
|
|
6296
|
-
const outputContent = outputLinesArr.join("\n");
|
|
6297
|
-
return {
|
|
6298
|
-
content: outputContent,
|
|
6299
|
-
truncated: true,
|
|
6300
|
-
truncatedBy,
|
|
6301
|
-
totalLines,
|
|
6302
|
-
totalBytes,
|
|
6303
|
-
outputLines: outputLinesArr.length,
|
|
6304
|
-
outputBytes: Buffer.byteLength(outputContent, "utf-8"),
|
|
6305
|
-
lastLinePartial: false,
|
|
6306
|
-
firstLineExceedsLimit: false,
|
|
6307
|
-
maxLines,
|
|
6308
|
-
maxBytes
|
|
6309
|
-
};
|
|
6310
|
-
}
|
|
6311
|
-
var fileMutationQueues = /* @__PURE__ */ new Map();
|
|
6312
|
-
var registrationQueue = Promise.resolve();
|
|
6313
|
-
function isMissingPathError(error) {
|
|
6314
|
-
return typeof error === "object" && error !== null && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
6315
|
-
}
|
|
6316
|
-
async function getMutationQueueKey(filePath) {
|
|
6317
|
-
const resolvedPath = resolve4(filePath);
|
|
6318
|
-
try {
|
|
6319
|
-
return await realpath(resolvedPath);
|
|
6320
|
-
} catch (error) {
|
|
6321
|
-
if (isMissingPathError(error)) return resolvedPath;
|
|
6322
|
-
throw error;
|
|
6323
|
-
}
|
|
6324
|
-
}
|
|
6325
|
-
async function withFileMutationQueue(filePath, run) {
|
|
6326
|
-
const registration = registrationQueue.then(async () => {
|
|
6327
|
-
const key2 = await getMutationQueueKey(filePath);
|
|
6328
|
-
const currentQueue2 = fileMutationQueues.get(key2) ?? Promise.resolve();
|
|
6329
|
-
let releaseNext2 = () => {
|
|
6330
|
-
};
|
|
6331
|
-
const nextQueue = new Promise((resolveQueue) => {
|
|
6332
|
-
releaseNext2 = resolveQueue;
|
|
6333
|
-
});
|
|
6334
|
-
const chainedQueue2 = currentQueue2.then(() => nextQueue);
|
|
6335
|
-
fileMutationQueues.set(key2, chainedQueue2);
|
|
6336
|
-
return { key: key2, currentQueue: currentQueue2, chainedQueue: chainedQueue2, releaseNext: releaseNext2 };
|
|
6337
|
-
});
|
|
6338
|
-
registrationQueue = registration.then(
|
|
6339
|
-
() => void 0,
|
|
6340
|
-
() => void 0
|
|
6341
|
-
);
|
|
6342
|
-
const { key, currentQueue, chainedQueue, releaseNext } = await registration;
|
|
6343
|
-
await currentQueue;
|
|
6344
|
-
try {
|
|
6345
|
-
return await run();
|
|
6346
|
-
} finally {
|
|
6347
|
-
releaseNext();
|
|
6348
|
-
if (fileMutationQueues.get(key) === chainedQueue) fileMutationQueues.delete(key);
|
|
6349
|
-
}
|
|
6350
|
-
}
|
|
6351
|
-
|
|
6352
6375
|
// src/served-session/serve-render.ts
|
|
6353
6376
|
function buildRangeServeRows(startLine, endLine, fileHashes) {
|
|
6354
6377
|
const total = endLine - startLine + 1;
|
|
@@ -7517,14 +7540,6 @@ function changedRange(original, result) {
|
|
|
7517
7540
|
};
|
|
7518
7541
|
}
|
|
7519
7542
|
|
|
7520
|
-
// src/file-content/served-header.ts
|
|
7521
|
-
function stopSuffix(stoppedAtBytes) {
|
|
7522
|
-
return stoppedAtBytes === void 0 ? "" : `; stopped at ${formatSize(stoppedAtBytes)}`;
|
|
7523
|
-
}
|
|
7524
|
-
function formatSectionHeader(startLine, endLine, totalLines, stoppedAtBytes) {
|
|
7525
|
-
return `[Lines ${startLine}-${endLine} of ${totalLines} total${stopSuffix(stoppedAtBytes)}]`;
|
|
7526
|
-
}
|
|
7527
|
-
|
|
7528
7543
|
// src/file-content/preview.ts
|
|
7529
7544
|
function normPosInt(value, name2) {
|
|
7530
7545
|
if (value === void 0) {
|
|
@@ -10861,6 +10876,7 @@ function usablePath(value) {
|
|
|
10861
10876
|
if (value === void 0) return void 0;
|
|
10862
10877
|
const path = value.trim();
|
|
10863
10878
|
if (path.length === 0 || path.startsWith("-")) return void 0;
|
|
10879
|
+
if (path.startsWith("$")) return void 0;
|
|
10864
10880
|
if (/[*?[\]]/u.test(path)) return void 0;
|
|
10865
10881
|
return path;
|
|
10866
10882
|
}
|
|
@@ -10922,26 +10938,37 @@ function aliasPath(stage) {
|
|
|
10922
10938
|
if (stage.length !== 2) return void 0;
|
|
10923
10939
|
return usablePath(stage[1]);
|
|
10924
10940
|
}
|
|
10925
|
-
function
|
|
10941
|
+
function selectObjectBounds(stage) {
|
|
10926
10942
|
if (stage[0] === void 0 || !eq(stage[0], "select-object")) return void 0;
|
|
10927
|
-
|
|
10943
|
+
const seen = {};
|
|
10928
10944
|
for (let index = 1; index < stage.length; index += 1) {
|
|
10929
10945
|
const token = stage[index];
|
|
10930
|
-
if (token === void 0
|
|
10946
|
+
if (token === void 0) return void 0;
|
|
10931
10947
|
const lower = token.toLowerCase();
|
|
10932
|
-
let
|
|
10933
|
-
if (lower === "-first")
|
|
10934
|
-
else if (lower === "-last")
|
|
10935
|
-
else if (lower === "-skip")
|
|
10948
|
+
let key;
|
|
10949
|
+
if (lower === "-first") key = "first";
|
|
10950
|
+
else if (lower === "-last") key = "last";
|
|
10951
|
+
else if (lower === "-skip") key = "skip";
|
|
10936
10952
|
else return void 0;
|
|
10937
10953
|
const raw = stage[index + 1];
|
|
10938
10954
|
index += 1;
|
|
10939
10955
|
if (raw === void 0 || !/^\d+$/u.test(raw)) return void 0;
|
|
10956
|
+
if (seen[key] !== void 0) return void 0;
|
|
10940
10957
|
const count = Number(raw);
|
|
10941
10958
|
if (!Number.isSafeInteger(count)) return void 0;
|
|
10942
|
-
|
|
10959
|
+
seen[key] = count;
|
|
10960
|
+
}
|
|
10961
|
+
const skip = seen.skip;
|
|
10962
|
+
const firstBound = seen.first;
|
|
10963
|
+
const lastBound = seen.last;
|
|
10964
|
+
if (lastBound !== void 0 && (skip !== void 0 || firstBound !== void 0)) {
|
|
10965
|
+
return void 0;
|
|
10943
10966
|
}
|
|
10944
|
-
return
|
|
10967
|
+
if (skip !== void 0 && firstBound !== void 0) return { skip, first: firstBound };
|
|
10968
|
+
if (skip !== void 0) return { skip };
|
|
10969
|
+
if (firstBound !== void 0) return { first: firstBound };
|
|
10970
|
+
if (lastBound !== void 0) return { last: lastBound };
|
|
10971
|
+
return void 0;
|
|
10945
10972
|
}
|
|
10946
10973
|
function selectStringPath(stage) {
|
|
10947
10974
|
if (stage[0] === void 0 || !eq(stage[0], "select-string")) return void 0;
|
|
@@ -11030,9 +11057,23 @@ function parse2(command) {
|
|
|
11030
11057
|
const second = stages[1];
|
|
11031
11058
|
if (stages.length !== 2 || second === void 0 || !eq(head, "get-content")) return void 0;
|
|
11032
11059
|
const path = getContentPath(first);
|
|
11033
|
-
const
|
|
11034
|
-
if (path === void 0 ||
|
|
11035
|
-
|
|
11060
|
+
const bounds = selectObjectBounds(second);
|
|
11061
|
+
if (path === void 0 || bounds === void 0) return void 0;
|
|
11062
|
+
const firstBound = bounds.first;
|
|
11063
|
+
const lastBound = bounds.last;
|
|
11064
|
+
if (bounds.skip !== void 0 && firstBound !== void 0) {
|
|
11065
|
+
return { kind: "window", path, skip: bounds.skip, count: firstBound };
|
|
11066
|
+
}
|
|
11067
|
+
if (bounds.skip !== void 0 && firstBound === void 0 && lastBound === void 0) {
|
|
11068
|
+
return { kind: "skip", path, count: bounds.skip };
|
|
11069
|
+
}
|
|
11070
|
+
if (firstBound !== void 0 && bounds.skip === void 0 && lastBound === void 0) {
|
|
11071
|
+
return { kind: "first", path, count: firstBound };
|
|
11072
|
+
}
|
|
11073
|
+
if (lastBound !== void 0 && bounds.skip === void 0 && firstBound === void 0) {
|
|
11074
|
+
return { kind: "last", path, count: lastBound };
|
|
11075
|
+
}
|
|
11076
|
+
return void 0;
|
|
11036
11077
|
}
|
|
11037
11078
|
function firstGreater(sorted, after) {
|
|
11038
11079
|
let low = 0;
|
|
@@ -11148,6 +11189,11 @@ function alignTransform(command, fileLines, stdoutLines) {
|
|
|
11148
11189
|
return stdoutLines.length === total ? aligned : void 0;
|
|
11149
11190
|
}
|
|
11150
11191
|
if (command.kind === "search") return aligned;
|
|
11192
|
+
if (command.kind === "window") {
|
|
11193
|
+
const window = Math.min(command.count, Math.max(0, total - command.skip));
|
|
11194
|
+
if (stdoutLines.length !== window) return void 0;
|
|
11195
|
+
return aligned[0] === command.skip + 1 ? aligned : void 0;
|
|
11196
|
+
}
|
|
11151
11197
|
const expected = command.kind === "skip" ? Math.max(0, total - command.count) : Math.min(command.count, total);
|
|
11152
11198
|
if (stdoutLines.length !== expected) return void 0;
|
|
11153
11199
|
if (command.kind === "first") return aligned[0] === 1 ? aligned : void 0;
|
|
@@ -11189,7 +11235,16 @@ async function plan(deps, request) {
|
|
|
11189
11235
|
if (row === void 0) return void 0;
|
|
11190
11236
|
rows.push(`${row.hash}${HASH_SEP}${row.text}`);
|
|
11191
11237
|
}
|
|
11192
|
-
|
|
11238
|
+
const startLine = lineNumbers[0];
|
|
11239
|
+
const endLine = lineNumbers[lineNumbers.length - 1];
|
|
11240
|
+
if (startLine === void 0 || endLine === void 0) return void 0;
|
|
11241
|
+
const header = formatPageHeader({
|
|
11242
|
+
label: BASH_VIEW_LABEL,
|
|
11243
|
+
file: command.path,
|
|
11244
|
+
totalLines: fileLines.length,
|
|
11245
|
+
shownRange: { startLine, endLine }
|
|
11246
|
+
});
|
|
11247
|
+
return { path: command.path, cwd: request.cwd, lineNumbers, header, rows };
|
|
11193
11248
|
} catch (error) {
|
|
11194
11249
|
reportDiagnostic("shell-view", "Could not serve the shell view; passing through:", error);
|
|
11195
11250
|
return void 0;
|
|
@@ -11284,7 +11339,8 @@ function createShellHooks(deps) {
|
|
|
11284
11339
|
return decision;
|
|
11285
11340
|
}
|
|
11286
11341
|
if (planned === void 0 || planned.rows.length === 0) return decision;
|
|
11287
|
-
const anchoredText = `${planned.
|
|
11342
|
+
const anchoredText = `${planned.header}
|
|
11343
|
+
${planned.rows.join("\n")}
|
|
11288
11344
|
`;
|
|
11289
11345
|
deps.metrics?.record({
|
|
11290
11346
|
tool: "shell_view",
|
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Three steps, each fail-closed:
|
|
6
6
|
* - {@link parse} admits only commands whose output is the file's own lines
|
|
7
|
-
* (`Get-Content`/`type`/`cat`,
|
|
8
|
-
*
|
|
7
|
+
* (`Get-Content`/`type`/`cat`, a `Select-Object -First|-Last|-Skip` bound or
|
|
8
|
+
* the `-Skip N -First M` window, `Select-String`, and `rg` with one explicit file);
|
|
9
9
|
* - {@link align} maps the stdout lines back to disk lines with a monotonic
|
|
10
10
|
* match that must be unique — duplicate content without a unique position is
|
|
11
11
|
* ambiguous and refuses;
|
|
12
12
|
* - {@link plan} serves the aligned lines through the read tool's seam (governed
|
|
13
|
-
* decode, stat adoption, snapshot materialization, leases)
|
|
13
|
+
* decode, stat adoption, snapshot materialization, leases), prints the shared
|
|
14
|
+
* `[Bash view: …]` page header naming the served range, and returns
|
|
14
15
|
* `HASH│content` rows.
|
|
15
16
|
*
|
|
16
17
|
* Anything unmodelled — an unknown pipe, redirection, a wildcard path, formatted
|
|
@@ -24,10 +25,17 @@ export type ParsedShellCommand = {
|
|
|
24
25
|
readonly kind: "full";
|
|
25
26
|
readonly path: string;
|
|
26
27
|
} | {
|
|
28
|
+
/** One `Select-Object` bound. */
|
|
27
29
|
readonly kind: "first" | "last" | "skip";
|
|
28
30
|
readonly path: string;
|
|
29
31
|
/** The `-First`/`-Last`/`-Skip` argument. */
|
|
30
32
|
readonly count: number;
|
|
33
|
+
} | {
|
|
34
|
+
/** `Select-Object -Skip N -First M`: disk lines N+1 through N+M. */
|
|
35
|
+
readonly kind: "window";
|
|
36
|
+
readonly path: string;
|
|
37
|
+
readonly skip: number;
|
|
38
|
+
readonly count: number;
|
|
31
39
|
} | {
|
|
32
40
|
readonly kind: "search";
|
|
33
41
|
readonly path: string;
|
|
@@ -49,6 +57,8 @@ export interface ShellViewPlan {
|
|
|
49
57
|
readonly cwd: string;
|
|
50
58
|
/** 1-based disk line numbers, ascending. */
|
|
51
59
|
readonly lineNumbers: number[];
|
|
60
|
+
/** The shared `[Bash view: …]` page header naming the served range. */
|
|
61
|
+
readonly header: string;
|
|
52
62
|
/** `HASH│content` rows for those lines, in line order. */
|
|
53
63
|
readonly rows: string[];
|
|
54
64
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHY: one definition site for the bash seams' bracket roots (#150, read-chrome): `view`, `search`,
|
|
3
|
+
* WHY: `file list` and `auto-read` each carry a kind word — the kind word IS the completeness claim —
|
|
4
|
+
* WHY: and that literal used to be copied at seven call sites in `src/lifecycle-hooks/index.ts`. Every
|
|
5
|
+
* WHY: site names its kind here instead, so the text below is the single definition site spec §3.4
|
|
6
|
+
* WHY: names. The `---` banner is gone: a page's chrome is its header, and only these two roots
|
|
7
|
+
* WHY: (a search's match list, a path list) have no page to name.
|
|
8
|
+
*/
|
|
9
|
+
export declare const BASH_SEARCH_ROOT = "[Bash search (anchors)]";
|
|
10
|
+
export declare const BASH_FILE_LIST_ROOT = "[Bash file list (no anchors)]";
|
|
11
|
+
/**
|
|
12
|
+
* WHY: `view` and `auto-read` merged into their page headers (`[Bash view: f (40 lines total)]`), so
|
|
13
|
+
* WHY: their frozen text is the label, not a line of its own — the completeness claim moves into the
|
|
14
|
+
* WHY: header's bracket and a page can no longer carry two of them.
|
|
15
|
+
*/
|
|
16
|
+
export declare const BASH_VIEW_LABEL = "Bash view";
|
|
17
|
+
export declare const BASH_AUTO_READ_LABEL = "Auto-read";
|
|
18
|
+
export type BashBannerKind = "search" | "file-list";
|
|
19
|
+
/** The bracket root that introduces a result with no page header of its own. */
|
|
20
|
+
export declare function bashBanner(kind: BashBannerKind): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-hashline-edit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Hash-anchored read/edit/undo_last_edit tools for DeepSeek Harness (dsh). Every line gets a unique 4-char hash (A-Za-z0-9) that stays stable across edits; stale or ambiguous anchors are rejected, never fuzzy-matched. Undo persists across restarts.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"anchors",
|