dsh-hashline-edit 0.1.0 → 0.2.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 +72 -70
- package/cordis.patch.yml +1 -1
- package/lib/index.js +4494 -4813
- package/lib/types/dsh/contracts.d.ts +69 -6
- package/lib/types/dsh/fs.d.ts +38 -3
- package/lib/types/dsh/guidance.d.ts +5 -5
- package/lib/types/dsh/hide.d.ts +78 -0
- package/lib/types/dsh/host-compat.d.ts +7 -78
- package/lib/types/dsh/index.d.ts +17 -7
- package/lib/types/dsh/metrics/events.d.ts +8 -1
- package/lib/types/dsh/metrics/instrument.d.ts +20 -14
- package/lib/types/dsh/metrics/observer.d.ts +28 -0
- package/lib/types/dsh/sandbox.d.ts +2 -2
- package/lib/types/dsh/shell-hook.d.ts +17 -8
- package/lib/types/dsh/shell-view.d.ts +65 -3
- package/lib/types/dsh/tools/apply-patch.d.ts +38 -0
- package/lib/types/dsh/tools/context.d.ts +19 -1
- package/lib/types/dsh/tools/output.d.ts +32 -4
- package/lib/types/dsh/tools/read.d.ts +12 -15
- package/lib/types/dsh/tools/undo.d.ts +5 -0
- package/lib/types/dsh/write-hook.d.ts +8 -22
- package/lib/types/src/apply-patch/commit.d.ts +49 -0
- package/lib/types/src/apply-patch/grammar.d.ts +29 -0
- package/lib/types/src/apply-patch/lifecycle.d.ts +32 -0
- package/lib/types/src/apply-patch/plan.d.ts +50 -0
- package/lib/types/src/apply-patch/render.d.ts +51 -0
- package/lib/types/src/apply-patch/resolve.d.ts +129 -0
- package/lib/types/src/apply-patch/tool.d.ts +54 -0
- package/lib/types/src/apply-patch/types.d.ts +362 -0
- package/lib/types/src/bash-view-banner.d.ts +20 -0
- package/lib/types/src/domain-errors.d.ts +16 -2
- package/lib/types/src/edit-undo.d.ts +11 -2
- package/lib/types/src/file-content/detection.d.ts +17 -0
- package/lib/types/src/file-content/index.d.ts +2 -2
- package/lib/types/src/file-content/preview.d.ts +1 -1
- package/lib/types/src/fs-lifecycle.d.ts +44 -0
- package/lib/types/src/hashline/resolve.d.ts +6 -0
- package/lib/types/src/host-utils.d.ts +63 -0
- package/lib/types/src/mutation-engine/batch-span-gate.d.ts +6 -0
- package/lib/types/src/mutation-engine/edit-request.d.ts +34 -0
- package/lib/types/src/mutation-engine/pipeline.d.ts +18 -3
- package/lib/types/src/mutation-engine/types.d.ts +50 -58
- package/lib/types/src/read-expansion/grammar-manifest.d.ts +4 -2
- package/lib/types/src/read-expansion/java-decoration.d.ts +21 -0
- package/lib/types/src/read-expansion/span-expansion.d.ts +6 -2
- package/lib/types/src/read-expansion/span-kind.d.ts +3 -2
- package/lib/types/src/read-expansion/vue-decoration.d.ts +14 -0
- package/lib/types/src/served-session/serve-render.d.ts +9 -3
- package/lib/types/src/undo-store.d.ts +17 -1
- package/package.json +10 -22
- package/prompts/apply-patch-guidelines.md +10 -0
- package/prompts/apply-patch.md +1 -0
- package/prompts/read-guidelines.md +5 -5
- package/prompts/read.md +1 -1
- package/prompts/undo-last-edit-guidelines.md +2 -2
- package/prompts/undo-last-edit-snippet.md +1 -1
- package/prompts/undo-last-edit.md +1 -1
- package/src/apply-patch/commit.ts +190 -0
- package/src/apply-patch/grammar.ts +620 -0
- package/src/apply-patch/index.ts +17 -0
- package/src/apply-patch/lifecycle.ts +195 -0
- package/src/apply-patch/plan.ts +800 -0
- package/src/apply-patch/render.ts +137 -0
- package/src/apply-patch/resolve.ts +470 -0
- package/src/apply-patch/tool.ts +257 -0
- package/src/apply-patch/types.ts +387 -0
- package/src/cut-repair.ts +77 -11
- package/src/domain-errors.ts +58 -10
- package/src/edit-undo.ts +379 -25
- package/src/file-content/detection.ts +34 -0
- package/src/file-content/index.ts +2 -2
- package/src/file-content/preview.ts +1 -1
- package/src/file-content/served-header.ts +1 -1
- package/src/fs-lifecycle.ts +201 -0
- package/src/hash-store.ts +18 -1
- package/src/hashline/lease-resolve.ts +14 -8
- package/src/hashline/resolve.ts +30 -27
- package/src/host-utils.ts +224 -0
- package/src/mutation-engine/batch-span-gate.ts +17 -4
- package/src/mutation-engine/edit-request.ts +39 -0
- package/src/mutation-engine/pipeline.ts +535 -63
- package/src/mutation-engine/types.ts +49 -65
- package/src/mutation-queue.ts +1 -1
- package/src/prompts.ts +0 -28
- package/src/read-expansion/ast-provider.ts +4 -0
- package/src/read-expansion/grammar-manifest.ts +21 -2
- package/src/read-expansion/java-decoration.ts +34 -0
- package/src/read-expansion/span-expansion.ts +6 -2
- package/src/read-expansion/span-kind.ts +3 -1
- package/src/read-expansion/vue-decoration.ts +43 -0
- package/src/served-session/serve-render.ts +5 -5
- package/src/undo-store.ts +74 -5
- package/lib/types/dsh/encoding-bridge.d.ts +0 -25
- package/lib/types/dsh/encoding.d.ts +0 -144
- package/lib/types/dsh/tools/edit.d.ts +0 -10
- package/lib/types/src/edit-tool.d.ts +0 -41
- package/lib/types/src/mutation-engine/engine.d.ts +0 -31
- package/lib/types/src/payload-contract.d.ts +0 -124
- package/prompts/edit-guidelines.md +0 -10
- package/prompts/edit-snippet.md +0 -1
- package/prompts/edit.md +0 -1
- package/src/bash-classifier.ts +0 -1626
- package/src/bash-conversion/decide.ts +0 -812
- package/src/bash-display-transform.ts +0 -50
- package/src/bash-multifile-config.ts +0 -239
- package/src/bash-view-config.ts +0 -158
- package/src/edit-normalize.ts +0 -8
- package/src/edit-render.ts +0 -161
- package/src/edit-tool.ts +0 -116
- package/src/edit.ts +0 -136
- package/src/integrations/pi-lens/command.ts +0 -712
- package/src/integrations/pi-lens/config.ts +0 -148
- package/src/integrations/pi-lens/io-bridge-adapter.ts +0 -234
- package/src/integrations/pi-lens/io-bridge.ts +0 -112
- package/src/integrations/pi-lens/settings-overlay.ts +0 -272
- package/src/lifecycle-hooks/index.ts +0 -1992
- package/src/lifecycle-hooks/types.ts +0 -64
- package/src/mutated-files.ts +0 -75
- package/src/mutation-engine/engine.ts +0 -173
- package/src/mutation-engine/index.ts +0 -17
- package/src/payload-contract.ts +0 -637
- package/src/preview-controller.ts +0 -84
- package/src/read.ts +0 -865
- package/src/search-hook.ts +0 -41
- package/src/store-config.ts +0 -73
- package/src/store-janitor-warning.ts +0 -169
- package/src/store-janitor.ts +0 -539
- package/src/store-kernel/settings.ts +0 -233
- package/src/store.ts +0 -154
- package/src/tui-presenter.ts +0 -134
- package/src/write-hook.ts +0 -130
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { promisify } from "node:util";
|
|
6
|
-
import type { DisplayTransform } from "./bash-classifier.js";
|
|
7
|
-
|
|
8
|
-
const execFileAsync = promisify(execFile);
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* WHY: (ADR-0048 D2) the transform is a closed display set. The re-run is the oracle: the same
|
|
12
|
-
* executable and flags run on a temporary copy of the exact raw head bytes, so platform-specific
|
|
13
|
-
* `od`/`xxd`/`hexdump` layouts cannot drift between what bash printed and what this proves.
|
|
14
|
-
* `cat -A` is GNU-only; `cat -vet` is the BSD/GNU-portable equivalent. A missing command, a
|
|
15
|
-
* timeout, or over-budget output returns undefined and the serve seam passes the whole command
|
|
16
|
-
* through raw with zero leases.
|
|
17
|
-
*/
|
|
18
|
-
const COMMANDS: Record<DisplayTransform["kind"], readonly [string, ...string[]]> = {
|
|
19
|
-
"cat-A": ["cat", "-A"],
|
|
20
|
-
"cat-vet": ["cat", "-vet"],
|
|
21
|
-
"od-c": ["od", "-c"],
|
|
22
|
-
xxd: ["xxd"],
|
|
23
|
-
"hexdump-C": ["hexdump", "-C"],
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const MAX_OUTPUT_BYTES = 8 * 1024 * 1024;
|
|
27
|
-
const TIMEOUT_MS = 5_000;
|
|
28
|
-
|
|
29
|
-
export async function runDisplayTransform(
|
|
30
|
-
transform: DisplayTransform,
|
|
31
|
-
input: string,
|
|
32
|
-
): Promise<string | undefined> {
|
|
33
|
-
const [program, ...args] = COMMANDS[transform.kind];
|
|
34
|
-
let dir: string | undefined;
|
|
35
|
-
try {
|
|
36
|
-
dir = await mkdtemp(join(tmpdir(), "pi-bash-display-"));
|
|
37
|
-
const file = join(dir, "slice");
|
|
38
|
-
await writeFile(file, input);
|
|
39
|
-
const { stdout } = await execFileAsync(program, [...args, file], {
|
|
40
|
-
encoding: "utf8",
|
|
41
|
-
maxBuffer: MAX_OUTPUT_BYTES,
|
|
42
|
-
timeout: TIMEOUT_MS,
|
|
43
|
-
});
|
|
44
|
-
return stdout;
|
|
45
|
-
} catch {
|
|
46
|
-
return undefined;
|
|
47
|
-
} finally {
|
|
48
|
-
if (dir !== undefined) await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
49
|
-
}
|
|
50
|
-
}
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `bash.multiFile` — the multi-file view caps (ADR-0049 M6), the sibling of `bash.view`.
|
|
3
|
-
*
|
|
4
|
-
* WHY the caps are settings: they are operator policy over how much one bash result may spend, not
|
|
5
|
-
* a correctness bound (the ADR's safety bound IS the allowed range — a config can only lower a cap,
|
|
6
|
-
* never raise it past the hard maximum). They resolve per KEY through the shared project/global
|
|
7
|
-
* ladder so "the project file is set" never stands in for "this key came from the project file".
|
|
8
|
-
*
|
|
9
|
-
* WHY an out-of-range value warns instead of falling through silently: ADR-0049 M6 requires the
|
|
10
|
-
* warning, and a dropped cap changes how much the session serves — the silent drop this ladder
|
|
11
|
-
* refuses to make at file level is the same drop one level down. A MISSING key still warns about
|
|
12
|
-
* nothing, and a broken file keeps warning exactly once through `readScopeConfig`.
|
|
13
|
-
*
|
|
14
|
-
* This module never touches the filesystem itself: reads go through `readScopeConfig`, writes
|
|
15
|
-
* through `updateConfigFile`, so the config file keeps one owner.
|
|
16
|
-
*/
|
|
17
|
-
import { DomainError } from "./domain-errors.js";
|
|
18
|
-
import {
|
|
19
|
-
configFilePath,
|
|
20
|
-
isConfigScope,
|
|
21
|
-
isRecord,
|
|
22
|
-
readScopeConfig,
|
|
23
|
-
updateConfigFile,
|
|
24
|
-
type ConfigOnWarn,
|
|
25
|
-
type ConfigRung,
|
|
26
|
-
type ConfigScope,
|
|
27
|
-
} from "./json-config.js";
|
|
28
|
-
|
|
29
|
-
/** The two caps, both integers in their allowed range. */
|
|
30
|
-
export interface BashMultiFileSettings {
|
|
31
|
-
readonly maxFiles: number;
|
|
32
|
-
readonly maxRows: number;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** One scope file's accepted keys; every key is optional and absent means "not set here". */
|
|
36
|
-
export type BashMultiFilePatch = Partial<BashMultiFileSettings>;
|
|
37
|
-
|
|
38
|
-
/** The key ADR-0049 M6 puts under `bash`. */
|
|
39
|
-
const BASH_MULTI_FILE_SUBTREE = "multiFile";
|
|
40
|
-
|
|
41
|
-
/** The per-key names, in report order. */
|
|
42
|
-
const BASH_MULTI_FILE_KEYS = {
|
|
43
|
-
maxFiles: "maxFiles",
|
|
44
|
-
maxRows: "maxRows",
|
|
45
|
-
} as const;
|
|
46
|
-
|
|
47
|
-
/** The declared defaults: an unconfigured session gets both. */
|
|
48
|
-
export const BASH_MULTI_FILE_DEFAULTS: BashMultiFileSettings = { maxFiles: 10, maxRows: 2000 };
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* WHY the ranges are closed at the top: ADR-0049 M6 fixes them, and the upper bound is the safety
|
|
52
|
-
* bound (the row cap fits the built-in bash result cap). Lowering is allowed — an operator may want
|
|
53
|
-
* a tighter page — raising is not a supported way to exceed it.
|
|
54
|
-
*/
|
|
55
|
-
export const BASH_MULTI_FILE_LIMITS = {
|
|
56
|
-
maxFiles: { min: 2, max: 10 },
|
|
57
|
-
maxRows: { min: 1, max: 2000 },
|
|
58
|
-
} as const;
|
|
59
|
-
|
|
60
|
-
type BashMultiFileKey = keyof BashMultiFileSettings;
|
|
61
|
-
|
|
62
|
-
const KEY_ORDER: readonly BashMultiFileKey[] = [
|
|
63
|
-
BASH_MULTI_FILE_KEYS.maxFiles,
|
|
64
|
-
BASH_MULTI_FILE_KEYS.maxRows,
|
|
65
|
-
];
|
|
66
|
-
|
|
67
|
-
/** One cap's rung: the value the session will use and where it came from. */
|
|
68
|
-
interface ResolvedBashMultiFileCap {
|
|
69
|
-
readonly value: number;
|
|
70
|
-
readonly source: ConfigRung;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Both caps with their rungs — the shape `/pi-better-edit settings` reports. */
|
|
74
|
-
export interface ResolvedBashMultiFileSettings {
|
|
75
|
-
readonly maxFiles: ResolvedBashMultiFileCap;
|
|
76
|
-
readonly maxRows: ResolvedBashMultiFileCap;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function withinRange(value: unknown, limit: { min: number; max: number }): value is number {
|
|
80
|
-
return (
|
|
81
|
-
typeof value === "number" && Number.isInteger(value) && value >= limit.min && value <= limit.max
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Runtime guard for `bash.multiFile.maxFiles` (integer, 2–10). */
|
|
86
|
-
export function isBashMultiFileMaxFiles(value: unknown): value is number {
|
|
87
|
-
return withinRange(value, BASH_MULTI_FILE_LIMITS.maxFiles);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Runtime guard for `bash.multiFile.maxRows` (integer, 1–2000). */
|
|
91
|
-
export function isBashMultiFileMaxRows(value: unknown): value is number {
|
|
92
|
-
return withinRange(value, BASH_MULTI_FILE_LIMITS.maxRows);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const GUARDS: Readonly<Record<BashMultiFileKey, (value: unknown) => value is number>> = {
|
|
96
|
-
maxFiles: isBashMultiFileMaxFiles,
|
|
97
|
-
maxRows: isBashMultiFileMaxRows,
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
/** Runtime guard for the two config scopes. */
|
|
101
|
-
export function isBashMultiFileScope(value: unknown): value is ConfigScope {
|
|
102
|
-
return isConfigScope(value);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** `2-10` / `1-2000`, the range text every warning and guard message uses. */
|
|
106
|
-
function rangeText(key: BashMultiFileKey): string {
|
|
107
|
-
const limit = BASH_MULTI_FILE_LIMITS[key];
|
|
108
|
-
return `${limit.min}-${limit.max}`;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* One scope file's accepted caps. Reads only; a key that is present but unusable is dropped WITH a
|
|
113
|
-
* warning naming the file, the key, the value and the allowed range, then falls through the ladder.
|
|
114
|
-
*/
|
|
115
|
-
function readScopeSettings(
|
|
116
|
-
scope: ConfigScope,
|
|
117
|
-
cwd: string,
|
|
118
|
-
onWarn?: ConfigOnWarn,
|
|
119
|
-
): BashMultiFilePatch {
|
|
120
|
-
const config = readScopeConfig(scope, cwd, onWarn);
|
|
121
|
-
if (config === undefined) return {};
|
|
122
|
-
const bash = config.bash;
|
|
123
|
-
if (!isRecord(bash)) return {};
|
|
124
|
-
const subtree = bash[BASH_MULTI_FILE_SUBTREE];
|
|
125
|
-
if (subtree === undefined) return {};
|
|
126
|
-
const path = configFilePath(scope, cwd);
|
|
127
|
-
if (!isRecord(subtree)) {
|
|
128
|
-
onWarn?.(`config ${path}: ignoring bash.${BASH_MULTI_FILE_SUBTREE} (not an object)`);
|
|
129
|
-
return {};
|
|
130
|
-
}
|
|
131
|
-
const accepted: Record<string, number> = {};
|
|
132
|
-
for (const key of KEY_ORDER) {
|
|
133
|
-
const raw = subtree[key];
|
|
134
|
-
if (raw === undefined) continue;
|
|
135
|
-
if (!GUARDS[key](raw)) {
|
|
136
|
-
onWarn?.(
|
|
137
|
-
`config ${path}: ignoring bash.${BASH_MULTI_FILE_SUBTREE}.${key} ${JSON.stringify(
|
|
138
|
-
raw,
|
|
139
|
-
)} (allowed ${rangeText(key)})`,
|
|
140
|
-
);
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
accepted[key] = raw;
|
|
144
|
-
}
|
|
145
|
-
return accepted;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Both scopes' accepted caps. A scope is absent (rather than an empty object) when its file is
|
|
150
|
-
* missing, carries no `bash.multiFile` object, or holds no accepted key.
|
|
151
|
-
*/
|
|
152
|
-
export function readBashMultiFileConfig(
|
|
153
|
-
cwd: string,
|
|
154
|
-
onWarn?: ConfigOnWarn,
|
|
155
|
-
): { project?: BashMultiFilePatch; global?: BashMultiFilePatch } {
|
|
156
|
-
const project = readScopeSettings("project", cwd, onWarn);
|
|
157
|
-
const global = readScopeSettings("global", cwd, onWarn);
|
|
158
|
-
return {
|
|
159
|
-
...(Object.keys(project).length > 0 ? { project } : {}),
|
|
160
|
-
...(Object.keys(global).length > 0 ? { global } : {}),
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* The effective caps WITH their rungs: project, then global, then the declared default — per key, so
|
|
166
|
-
* one key can come from a different rung than its sibling. Never throws.
|
|
167
|
-
*/
|
|
168
|
-
export function resolveBashMultiFileSettingsWithSources(
|
|
169
|
-
cwd: string,
|
|
170
|
-
onWarn?: ConfigOnWarn,
|
|
171
|
-
): ResolvedBashMultiFileSettings {
|
|
172
|
-
const config = readBashMultiFileConfig(cwd, onWarn);
|
|
173
|
-
const pick = (key: BashMultiFileKey): ResolvedBashMultiFileCap => {
|
|
174
|
-
const fromProject = config.project?.[key];
|
|
175
|
-
if (fromProject !== undefined) return { value: fromProject, source: "project" };
|
|
176
|
-
const fromGlobal = config.global?.[key];
|
|
177
|
-
if (fromGlobal !== undefined) return { value: fromGlobal, source: "global" };
|
|
178
|
-
return { value: BASH_MULTI_FILE_DEFAULTS[key], source: "default" };
|
|
179
|
-
};
|
|
180
|
-
return { maxFiles: pick("maxFiles"), maxRows: pick("maxRows") };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** Effective caps only — the ladder's value half. Never throws. */
|
|
184
|
-
export function resolveBashMultiFileSettings(
|
|
185
|
-
cwd: string,
|
|
186
|
-
onWarn?: ConfigOnWarn,
|
|
187
|
-
): BashMultiFileSettings {
|
|
188
|
-
const resolved = resolveBashMultiFileSettingsWithSources(cwd, onWarn);
|
|
189
|
-
return { maxFiles: resolved.maxFiles.value, maxRows: resolved.maxRows.value };
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Persist the given caps for `scope`, preserving `$schema`, unknown top-level keys, every other
|
|
194
|
-
* `bash.*` key (including `view`) and every `multiFile` key not being written. Throws
|
|
195
|
-
* `E_INTERNAL_INVARIANT` — and writes nothing — for an out-of-range value or an unknown scope: a
|
|
196
|
-
* defensive guard, unreachable from the command, because a silently dropped cap would read back as
|
|
197
|
-
* the default.
|
|
198
|
-
*/
|
|
199
|
-
export function saveBashMultiFileConfig(
|
|
200
|
-
patch: BashMultiFilePatch,
|
|
201
|
-
scope: ConfigScope,
|
|
202
|
-
cwd: string,
|
|
203
|
-
): { path: string } {
|
|
204
|
-
const keys = KEY_ORDER.filter((key) => patch[key] !== undefined);
|
|
205
|
-
if (keys.length === 0) {
|
|
206
|
-
throw new DomainError("E_INTERNAL_INVARIANT", {
|
|
207
|
-
message: `Missing bash.${BASH_MULTI_FILE_SUBTREE} setting to write.`,
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
for (const key of keys) {
|
|
211
|
-
const value = patch[key];
|
|
212
|
-
if (!GUARDS[key](value)) {
|
|
213
|
-
throw new DomainError("E_INTERNAL_INVARIANT", {
|
|
214
|
-
message: `Unknown bash.${BASH_MULTI_FILE_SUBTREE}.${key} "${String(value)}" (allowed ${rangeText(
|
|
215
|
-
key,
|
|
216
|
-
)}).`,
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
if (!isBashMultiFileScope(scope)) {
|
|
221
|
-
throw new DomainError("E_INTERNAL_INVARIANT", {
|
|
222
|
-
message: `Unknown bash.${BASH_MULTI_FILE_SUBTREE} scope "${String(scope)}".`,
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const path = configFilePath(scope, cwd);
|
|
227
|
-
updateConfigFile(path, (existing) => {
|
|
228
|
-
const bash = isRecord(existing.bash) ? existing.bash : {};
|
|
229
|
-
const subtree = isRecord(bash[BASH_MULTI_FILE_SUBTREE]) ? bash[BASH_MULTI_FILE_SUBTREE] : {};
|
|
230
|
-
return {
|
|
231
|
-
...existing,
|
|
232
|
-
bash: {
|
|
233
|
-
...bash,
|
|
234
|
-
[BASH_MULTI_FILE_SUBTREE]: { ...subtree, ...patch },
|
|
235
|
-
},
|
|
236
|
-
};
|
|
237
|
-
});
|
|
238
|
-
return { path };
|
|
239
|
-
}
|
package/src/bash-view-config.ts
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bash view setting — the `bash.view` key of the shared config file, resolved
|
|
3
|
-
* through the shared ladder: env override, then project file, then global file,
|
|
4
|
-
* then the default.
|
|
5
|
-
*
|
|
6
|
-
* WHY the interception is configurable at all: the view replacement swaps bash
|
|
7
|
-
* stdout for anchored lines, which is a display decision about the model's own
|
|
8
|
-
* session — an operator who would rather see raw output turns one key off, and
|
|
9
|
-
* every other session keeps the default without restating it.
|
|
10
|
-
*
|
|
11
|
-
* WHY a warning is part of the contract here: a dropped setting changes what the
|
|
12
|
-
* session shows, so an existing-but-unusable file is reported through `onWarn`
|
|
13
|
-
* while the value falls through the ladder. A missing file warns about nothing,
|
|
14
|
-
* and a value outside the accepted shape is ignored exactly as the sibling
|
|
15
|
-
* ladders ignore one. Nothing here throws on the read side.
|
|
16
|
-
*/
|
|
17
|
-
import { DomainError } from "./domain-errors.js";
|
|
18
|
-
import {
|
|
19
|
-
configFilePath,
|
|
20
|
-
isConfigScope,
|
|
21
|
-
isRecord,
|
|
22
|
-
readScopeConfig,
|
|
23
|
-
updateConfigFile,
|
|
24
|
-
type ConfigOnWarn,
|
|
25
|
-
type ConfigRung,
|
|
26
|
-
type ConfigScope,
|
|
27
|
-
} from "./json-config.js";
|
|
28
|
-
|
|
29
|
-
export type BashViewMode = "on" | "off";
|
|
30
|
-
|
|
31
|
-
/** Environment override; wins over both files. Values outside the enum are ignored, not fatal. */
|
|
32
|
-
export const BASH_VIEW_ENV_VAR = "PI_BETTER_EDIT_BASH_VIEW";
|
|
33
|
-
|
|
34
|
-
/** The mode an unconfigured session gets: interception stays on. */
|
|
35
|
-
export const BASH_VIEW_DEFAULT: BashViewMode = "on";
|
|
36
|
-
|
|
37
|
-
const BASH_VIEW_MODES: readonly BashViewMode[] = ["on", "off"];
|
|
38
|
-
|
|
39
|
-
/** The single key this owner reads and writes under `bash`. */
|
|
40
|
-
const BASH_VIEW_KEY = "view";
|
|
41
|
-
|
|
42
|
-
/** Runtime guard for values arriving from files, env vars and command arguments. */
|
|
43
|
-
export function isBashViewMode(value: unknown): value is BashViewMode {
|
|
44
|
-
return typeof value === "string" && BASH_VIEW_MODES.some((mode) => mode === value);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Runtime guard for the two config scopes. */
|
|
48
|
-
export function isBashViewScope(value: unknown): value is ConfigScope {
|
|
49
|
-
return isConfigScope(value);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** One scope file's mode, or `undefined` when the file carries no accepted value. */
|
|
53
|
-
function readScopeMode(
|
|
54
|
-
scope: ConfigScope,
|
|
55
|
-
cwd: string,
|
|
56
|
-
onWarn?: ConfigOnWarn,
|
|
57
|
-
): BashViewMode | undefined {
|
|
58
|
-
const config = readScopeConfig(scope, cwd, onWarn);
|
|
59
|
-
if (config === undefined) return undefined;
|
|
60
|
-
const bash = config.bash;
|
|
61
|
-
if (!isRecord(bash)) return undefined;
|
|
62
|
-
const mode = bash[BASH_VIEW_KEY];
|
|
63
|
-
return isBashViewMode(mode) ? mode : undefined;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Both scopes' configured modes. Pure read: never creates the directory or the file, never
|
|
68
|
-
* throws. A scope is absent (rather than `undefined`-valued) when its file is missing or holds a
|
|
69
|
-
* value outside the enum.
|
|
70
|
-
*/
|
|
71
|
-
export function readBashViewConfig(
|
|
72
|
-
cwd: string,
|
|
73
|
-
onWarn?: ConfigOnWarn,
|
|
74
|
-
): { project?: BashViewMode; global?: BashViewMode } {
|
|
75
|
-
const project = readScopeMode("project", cwd, onWarn);
|
|
76
|
-
const global = readScopeMode("global", cwd, onWarn);
|
|
77
|
-
return {
|
|
78
|
-
...(project !== undefined ? { project } : {}),
|
|
79
|
-
...(global !== undefined ? { global } : {}),
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** A trimmed environment value, or `undefined` when the variable is unset. */
|
|
84
|
-
function envText(name: string): string | undefined {
|
|
85
|
-
const raw = process.env[name];
|
|
86
|
-
return raw === undefined ? undefined : raw.trim();
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function readEnvMode(): BashViewMode | undefined {
|
|
90
|
-
const mode = envText(BASH_VIEW_ENV_VAR)?.toLowerCase();
|
|
91
|
-
return mode !== undefined && isBashViewMode(mode) ? mode : undefined;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* One resolved setting: the value the session will use, and the rung of the ladder
|
|
96
|
-
* that supplied it.
|
|
97
|
-
*/
|
|
98
|
-
export interface ResolvedBashViewMode {
|
|
99
|
-
readonly value: BashViewMode;
|
|
100
|
-
readonly source: ConfigRung;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* The effective mode WITH its rung: env override, then project, then global, then `"on"`.
|
|
105
|
-
* Never throws. This is the ladder itself, and `resolveBashViewMode` is its value half, so the
|
|
106
|
-
* two cannot disagree about which rung won — a caller that has to NAME the winner (the
|
|
107
|
-
* `/pi-better-edit` status text does) reads it here instead of re-walking the two files.
|
|
108
|
-
*/
|
|
109
|
-
export function resolveBashViewModeWithSource(
|
|
110
|
-
cwd: string,
|
|
111
|
-
onWarn?: ConfigOnWarn,
|
|
112
|
-
): ResolvedBashViewMode {
|
|
113
|
-
const fromEnv = readEnvMode();
|
|
114
|
-
if (fromEnv !== undefined) return { value: fromEnv, source: "env" };
|
|
115
|
-
const config = readBashViewConfig(cwd, onWarn);
|
|
116
|
-
if (config.project !== undefined) return { value: config.project, source: "project" };
|
|
117
|
-
if (config.global !== undefined) return { value: config.global, source: "global" };
|
|
118
|
-
return { value: BASH_VIEW_DEFAULT, source: "default" };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Effective mode: env override, then project, then global, then `"on"`. Never throws. */
|
|
122
|
-
export function resolveBashViewMode(cwd: string, onWarn?: ConfigOnWarn): BashViewMode {
|
|
123
|
-
return resolveBashViewModeWithSource(cwd, onWarn).value;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Persist `mode` for `scope`, preserving unknown top-level keys and other `bash.*` keys already
|
|
128
|
-
* in the file. Creates the directory as needed and replaces the file atomically. Throws
|
|
129
|
-
* `E_INTERNAL_INVARIANT` — and writes nothing — for a value outside its accepted shape or a
|
|
130
|
-
* scope outside the enum: a defensive guard, unreachable from the command and the overlay,
|
|
131
|
-
* because a silently dropped value would read back as the default.
|
|
132
|
-
*/
|
|
133
|
-
export function saveBashViewConfig(
|
|
134
|
-
mode: BashViewMode,
|
|
135
|
-
scope: ConfigScope,
|
|
136
|
-
cwd: string,
|
|
137
|
-
): { path: string } {
|
|
138
|
-
if (!isBashViewMode(mode)) {
|
|
139
|
-
throw new DomainError("E_INTERNAL_INVARIANT", {
|
|
140
|
-
message: `Unknown bash view mode "${String(mode)}".`,
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
if (!isBashViewScope(scope)) {
|
|
144
|
-
throw new DomainError("E_INTERNAL_INVARIANT", {
|
|
145
|
-
message: `Unknown bash view scope "${String(scope)}".`,
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const path = configFilePath(scope, cwd);
|
|
150
|
-
// WHY: only `bash.view` is ours to set — an existing `$schema` is a deliberate editor-time
|
|
151
|
-
// WHY: choice and unknown top-level or `bash.*` keys belong to the user, so all of them survive.
|
|
152
|
-
updateConfigFile(path, (existing) => ({
|
|
153
|
-
...existing,
|
|
154
|
-
bash: { ...(isRecord(existing.bash) ? existing.bash : {}), [BASH_VIEW_KEY]: mode },
|
|
155
|
-
}));
|
|
156
|
-
|
|
157
|
-
return { path };
|
|
158
|
-
}
|
package/src/edit-normalize.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/** SAFETY: @deprecated Import from "./payload-contract.js" instead — compatibility shim retained until next MAJOR per ADR-0007. Single source: src/payload-contract.ts. */
|
|
2
|
-
/** SAFETY: @deprecated Use `import { normReq } from "./payload-contract.js"` */
|
|
3
|
-
export {
|
|
4
|
-
/** SAFETY: @deprecated Use `import { normReq } from "./payload-contract.js"` */
|
|
5
|
-
normReq,
|
|
6
|
-
/** SAFETY: @deprecated Use `import { prepareEditArguments } from "./payload-contract.js"` */
|
|
7
|
-
prepareEditArguments,
|
|
8
|
-
} from "./payload-contract.js";
|
package/src/edit-render.ts
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { getPreviewInput as contractPreviewInput, type EditItem } from "./payload-contract.js";
|
|
3
|
-
import type { EditDetails } from "./edit-response.js";
|
|
4
|
-
|
|
5
|
-
export type FgT = Pick<Theme, "fg">;
|
|
6
|
-
export type CallT = Pick<Theme, "fg" | "bold">;
|
|
7
|
-
export type MdTheme = Pick<Theme, "fg" | "bold" | "italic" | "underline" | "strikethrough">;
|
|
8
|
-
|
|
9
|
-
export type RPreview = { diff: string } | { error: string };
|
|
10
|
-
|
|
11
|
-
export type RRState = {
|
|
12
|
-
argsKey?: string;
|
|
13
|
-
preview?: RPreview;
|
|
14
|
-
previewGeneration?: number;
|
|
15
|
-
previewTimer?: ReturnType<typeof setTimeout>;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export function getPreviewInput(args: unknown): { file: string; edits: EditItem[] } | null {
|
|
19
|
-
return contractPreviewInput(args);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function colorLines(lines: string[], theme: FgT): string[] {
|
|
23
|
-
return lines.map((line) => {
|
|
24
|
-
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
25
|
-
return theme.fg("success", line);
|
|
26
|
-
}
|
|
27
|
-
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
28
|
-
return theme.fg("error", line);
|
|
29
|
-
}
|
|
30
|
-
return theme.fg("dim", line);
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function fmtPreview(diff: string, expanded: boolean, theme: FgT): string {
|
|
35
|
-
const lines = diff.split("\n");
|
|
36
|
-
const maxLines = expanded ? 40 : 16;
|
|
37
|
-
const shown = colorLines(lines.slice(0, maxLines), theme);
|
|
38
|
-
|
|
39
|
-
if (lines.length > maxLines) {
|
|
40
|
-
shown.push(theme.fg("muted", `... ${lines.length - maxLines} more diff lines`));
|
|
41
|
-
}
|
|
42
|
-
return shown.join("\n");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function fmtResult(diff: string, theme: FgT): string {
|
|
46
|
-
return colorLines(diff.split("\n"), theme).join("\n");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function fmtCall(
|
|
50
|
-
args: { file: string; edits: EditItem[] } | null,
|
|
51
|
-
state: RRState,
|
|
52
|
-
expanded: boolean,
|
|
53
|
-
theme: CallT,
|
|
54
|
-
): string {
|
|
55
|
-
const path = args?.file;
|
|
56
|
-
const pathDisplay =
|
|
57
|
-
typeof path === "string" && path.length > 0
|
|
58
|
-
? theme.fg("accent", path)
|
|
59
|
-
: theme.fg("toolOutput", "...");
|
|
60
|
-
const arity = args && args.edits.length > 1 ? ` (${args.edits.length} edits)` : "";
|
|
61
|
-
let text = `${theme.fg("toolTitle", theme.bold("edit"))} ${pathDisplay}${arity}`;
|
|
62
|
-
|
|
63
|
-
if (!state.preview) {
|
|
64
|
-
return text;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if ("error" in state.preview) {
|
|
68
|
-
text += `\n\n${theme.fg("error", state.preview.error)}`;
|
|
69
|
-
return text;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (state.preview.diff) {
|
|
73
|
-
text += `\n\n${fmtPreview(state.preview.diff, expanded, theme)}`;
|
|
74
|
-
}
|
|
75
|
-
return text;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function getResultText(result: {
|
|
79
|
-
content?: Array<{ type: string; text?: string }>;
|
|
80
|
-
}): string | undefined {
|
|
81
|
-
const textContent = result.content?.find(
|
|
82
|
-
(entry): entry is { type: "text"; text: string } =>
|
|
83
|
-
entry.type === "text" && typeof entry.text === "string",
|
|
84
|
-
);
|
|
85
|
-
return textContent?.text;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function isApplied(details: EditDetails | undefined): boolean {
|
|
89
|
-
const metrics = details?.metrics;
|
|
90
|
-
return (
|
|
91
|
-
metrics?.classification === "applied" &&
|
|
92
|
-
metrics.added_lines !== undefined &&
|
|
93
|
-
metrics.removed_lines !== undefined
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function buildAppliedText(details: EditDetails | undefined, theme: FgT): string | undefined {
|
|
98
|
-
const sections: string[] = [];
|
|
99
|
-
if (details?.diff) {
|
|
100
|
-
sections.push(fmtResult(details.diff, theme));
|
|
101
|
-
}
|
|
102
|
-
const warnings = details?.warnings;
|
|
103
|
-
if (warnings?.length) {
|
|
104
|
-
sections.push(theme.fg("dim", warnings.join("\n")));
|
|
105
|
-
}
|
|
106
|
-
const driftNotice = details?.driftNotice;
|
|
107
|
-
if (driftNotice) {
|
|
108
|
-
sections.push(theme.fg("dim", driftNotice));
|
|
109
|
-
}
|
|
110
|
-
return sections.length > 0 ? sections.join("\n\n") : undefined;
|
|
111
|
-
}
|
|
112
|
-
function trimEmpty(lines: string[]): string[] {
|
|
113
|
-
let start = 0;
|
|
114
|
-
let end = lines.length;
|
|
115
|
-
|
|
116
|
-
while (start < end && lines[start] === "") {
|
|
117
|
-
start++;
|
|
118
|
-
}
|
|
119
|
-
while (end > start && lines[end - 1] === "") {
|
|
120
|
-
end--;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return lines.slice(start, end);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function fmtResultMd(text: string): string {
|
|
127
|
-
return trimEmpty(text.split("\n")).join("\n");
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export function mkMdTheme(theme: MdTheme) {
|
|
131
|
-
return {
|
|
132
|
-
heading: (text: string) => theme.fg("mdHeading", text),
|
|
133
|
-
link: (text: string) => theme.fg("mdLink", text),
|
|
134
|
-
linkUrl: (text: string) => theme.fg("mdLinkUrl", text),
|
|
135
|
-
code: (text: string) => theme.fg("mdCode", text),
|
|
136
|
-
codeBlock: (text: string) => theme.fg("mdCodeBlock", text),
|
|
137
|
-
codeBlockBorder: (text: string) => theme.fg("mdCodeBlockBorder", text),
|
|
138
|
-
quote: (text: string) => theme.fg("mdQuote", text),
|
|
139
|
-
quoteBorder: (text: string) => theme.fg("mdQuoteBorder", text),
|
|
140
|
-
hr: (text: string) => theme.fg("mdHr", text),
|
|
141
|
-
listBullet: (text: string) => theme.fg("mdListBullet", text),
|
|
142
|
-
bold: (text: string) => theme.bold(text),
|
|
143
|
-
italic: (text: string) => (theme.italic ? theme.italic(text) : text),
|
|
144
|
-
underline: (text: string) => (theme.underline ? theme.underline(text) : text),
|
|
145
|
-
strikethrough: (text: string) => (theme.strikethrough ? theme.strikethrough(text) : text),
|
|
146
|
-
highlightCode: (code: string, lang?: string) =>
|
|
147
|
-
code.split("\n").map((line) => {
|
|
148
|
-
if (lang === "diff") {
|
|
149
|
-
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
150
|
-
return theme.fg("toolDiffAdded", line);
|
|
151
|
-
}
|
|
152
|
-
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
153
|
-
return theme.fg("toolDiffRemoved", line);
|
|
154
|
-
}
|
|
155
|
-
return theme.fg("toolDiffContext", line);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return theme.fg("mdCodeBlock", line);
|
|
159
|
-
}),
|
|
160
|
-
};
|
|
161
|
-
}
|