@xandreed/plugin-workspace 0.0.0-stage → 0.15.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/LICENSE +21 -0
- package/README.md +4 -2
- package/dist/bash-tool.adapter.d.ts +15 -0
- package/dist/bash-tool.adapter.js +73 -0
- package/dist/bash.adapter.d.ts +81 -0
- package/dist/bash.adapter.js +112 -0
- package/dist/file-system.adapter.d.ts +24 -0
- package/dist/file-system.adapter.js +52 -0
- package/dist/image-resizer.adapter.d.ts +5 -0
- package/dist/image-resizer.adapter.js +68 -0
- package/dist/image-resizer.entity.d.ts +23 -0
- package/dist/image-resizer.entity.functions.d.ts +15 -0
- package/dist/image-resizer.entity.functions.js +4 -0
- package/dist/image-resizer.entity.js +7 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +38 -0
- package/dist/local-system.entity.d.ts +47 -0
- package/dist/local-system.entity.functions.d.ts +53 -0
- package/dist/local-system.entity.functions.js +46 -0
- package/dist/local-system.entity.js +12 -0
- package/dist/plugin.adapter.d.ts +183 -0
- package/dist/plugin.adapter.js +38 -0
- package/dist/read-tools.adapter.d.ts +39 -0
- package/dist/read-tools.adapter.js +109 -0
- package/dist/ripgrep.adapter.d.ts +74 -0
- package/dist/ripgrep.adapter.js +56 -0
- package/dist/ripgrep.entity.d.ts +63 -0
- package/dist/ripgrep.entity.functions.d.ts +87 -0
- package/dist/ripgrep.entity.functions.js +62 -0
- package/dist/ripgrep.entity.js +10 -0
- package/dist/shell.entity.functions.d.ts +2 -0
- package/dist/shell.entity.functions.js +1 -0
- package/dist/skills/skills-directory.adapter.d.ts +68 -0
- package/dist/skills/skills-directory.adapter.js +30 -0
- package/dist/skills/skills-directory.entity.d.ts +99 -0
- package/dist/skills/skills-directory.entity.functions.d.ts +88 -0
- package/dist/skills/skills-directory.entity.functions.js +45 -0
- package/dist/skills/skills-directory.entity.js +35 -0
- package/dist/text-edit.entity.d.ts +32 -0
- package/dist/text-edit.entity.functions.d.ts +32 -0
- package/dist/text-edit.entity.functions.js +68 -0
- package/dist/text-edit.entity.js +15 -0
- package/dist/text-search.entity.d.ts +23 -0
- package/dist/text-search.entity.functions.d.ts +58 -0
- package/dist/text-search.entity.functions.js +30 -0
- package/dist/text-search.entity.js +10 -0
- package/dist/text-window.entity.d.ts +46 -0
- package/dist/text-window.entity.functions.d.ts +54 -0
- package/dist/text-window.entity.functions.js +88 -0
- package/dist/text-window.entity.js +21 -0
- package/dist/workspace-path.entity.d.ts +43 -0
- package/dist/workspace-path.entity.functions.d.ts +90 -0
- package/dist/workspace-path.entity.functions.js +88 -0
- package/dist/workspace-path.entity.js +9 -0
- package/dist/workspace-tools.adapter.d.ts +66 -0
- package/dist/workspace-tools.adapter.js +60 -0
- package/dist/workspace-tools.entity.d.ts +552 -0
- package/dist/workspace-tools.entity.functions.d.ts +126 -0
- package/dist/workspace-tools.entity.functions.js +180 -0
- package/dist/workspace-tools.entity.js +154 -0
- package/dist/write-tools.adapter.d.ts +12 -0
- package/dist/write-tools.adapter.js +79 -0
- package/package.json +30 -4
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/**
|
|
3
|
+
* How a text ends its lines: every break is `\r\n` (`crlf`), none has a
|
|
4
|
+
* `\r` (`lf`), or both kinds appear (`mixed`, matched as it is).
|
|
5
|
+
*/
|
|
6
|
+
export declare const LineEnding: Schema.Literals<readonly ["lf", "crlf", "mixed"]>;
|
|
7
|
+
export type LineEnding = typeof LineEnding.Type;
|
|
8
|
+
/**
|
|
9
|
+
* Edits applied to a text: `text` is the new text, in the original's line
|
|
10
|
+
* endings; `before` and `after` are the old and new text with `\n` line
|
|
11
|
+
* breaks (what a diff compares); `replaced` counts the edits, `normalized`
|
|
12
|
+
* says whether any was found only after normalising, and
|
|
13
|
+
* `firstChangedLine` is the 1-based line of the first change.
|
|
14
|
+
*/
|
|
15
|
+
export declare const EditPlan: Schema.Struct<{
|
|
16
|
+
readonly text: Schema.String;
|
|
17
|
+
readonly before: Schema.String;
|
|
18
|
+
readonly after: Schema.String;
|
|
19
|
+
readonly replaced: Schema.Int;
|
|
20
|
+
readonly normalized: Schema.Boolean;
|
|
21
|
+
readonly firstChangedLine: Schema.Int;
|
|
22
|
+
}>;
|
|
23
|
+
export type EditPlan = typeof EditPlan.Type;
|
|
24
|
+
/** Where one edit applies in the original: the span `[start, end)` and what replaces it. */
|
|
25
|
+
export declare const EditSpan: Schema.Struct<{
|
|
26
|
+
readonly index: Schema.Int;
|
|
27
|
+
readonly start: Schema.Int;
|
|
28
|
+
readonly end: Schema.Int;
|
|
29
|
+
readonly newText: Schema.String;
|
|
30
|
+
readonly normalized: Schema.Boolean;
|
|
31
|
+
}>;
|
|
32
|
+
export type EditSpan = typeof EditSpan.Type;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Result } from "effect";
|
|
2
|
+
import type { Failure } from "@xandreed/core";
|
|
3
|
+
import type { EditPlan, LineEnding } from "./text-edit.entity.js";
|
|
4
|
+
import type { TextEdit } from "./workspace-tools.entity.js";
|
|
5
|
+
/** How `text` ends its lines (see `LineEnding`). */
|
|
6
|
+
export declare const lineEndingOf: (text: string) => LineEnding;
|
|
7
|
+
/**
|
|
8
|
+
* A line as the fallback compares it: Unicode compatibility forms folded
|
|
9
|
+
* (NFKC, which also makes a no-break space a space), curly quotes and
|
|
10
|
+
* primes made straight, dashes and the minus sign made `-`, runs of
|
|
11
|
+
* whitespace made one space, and the ends trimmed.
|
|
12
|
+
*/
|
|
13
|
+
export declare const normalizeLine: (line: string) => string;
|
|
14
|
+
/**
|
|
15
|
+
* Apply edits to a file's text. Every edit is matched against the original,
|
|
16
|
+
* never against another edit's result: its `oldText` must appear exactly
|
|
17
|
+
* once (an empty one is refused), or, with `fuzzy`, once over whole lines
|
|
18
|
+
* after `normalizeLine`. The spans must not overlap. A text whose every
|
|
19
|
+
* line break is `\r\n` is matched with `\n` breaks (in it and in the edits)
|
|
20
|
+
* and written back with `\r\n`; a text without `\r` gets the edits' `\r\n`
|
|
21
|
+
* as `\n`. Edits that leave the text as it was fail with `NoChange`.
|
|
22
|
+
*/
|
|
23
|
+
export declare const applyTextEdits: (original: string, edits: ReadonlyArray<TextEdit>, options: {
|
|
24
|
+
readonly fuzzy: boolean;
|
|
25
|
+
}) => Result.Result<EditPlan, Failure>;
|
|
26
|
+
/**
|
|
27
|
+
* The change as a unified diff (`--- path` / `+++ path`), from effect's
|
|
28
|
+
* `TextDiff` with at most 10 000 edits searched per changed region (beyond
|
|
29
|
+
* that the diff stays valid, just not minimal), cut to `maxBytes` characters
|
|
30
|
+
* with a note when longer.
|
|
31
|
+
*/
|
|
32
|
+
export declare const unifiedDiff: (path: string, before: string, after: string, maxChars: number) => string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Option, Result, TextDiff } from "effect";
|
|
2
|
+
const failure = (error, message) => ({ error, message });
|
|
3
|
+
export const lineEndingOf = (text) => !text.includes("\r") ? "lf" : text.includes(`\r
|
|
4
|
+
`) && !/(^|[^\r])\n/.test(text) ? "crlf" : "mixed", normalizeLine = (line) => line.normalize("NFKC").replace(/[\u2018\u2019\u201A\u201B\u2032\u2035]/g, "'").replace(/[\u201C\u201D\u201E\u201F\u2033\u2036]/g, '"').replace(/[\u2010-\u2015\u2212]/g, "-").replace(/\s+/g, " ").trim();
|
|
5
|
+
const label = (index, count) => count === 1 ? "oldText" : `edit ${index + 1}'s oldText`, exactSpan = (base, edit, index, count) => {
|
|
6
|
+
const first = base.indexOf(edit.oldText);
|
|
7
|
+
if (first < 0)
|
|
8
|
+
return Result.succeed(Option.none());
|
|
9
|
+
if (base.indexOf(edit.oldText, first + 1) >= 0)
|
|
10
|
+
return Result.fail(failure("AmbiguousMatch", `${label(index, count)} appears ${Math.max(2, base.split(edit.oldText).length - 1)} times; add surrounding lines so it appears exactly once`));
|
|
11
|
+
return Result.succeed(Option.some({ index, start: first, end: first + edit.oldText.length, newText: edit.newText, normalized: !1 }));
|
|
12
|
+
}, normalizedSpan = (base, edit, index, count) => {
|
|
13
|
+
const withBreak = edit.oldText.endsWith(`
|
|
14
|
+
`), wantedLines = edit.oldText.split(`
|
|
15
|
+
`), wanted = (withBreak ? wantedLines.slice(0, -1) : wantedLines).map(normalizeLine);
|
|
16
|
+
if (wanted.every((line) => line.length === 0))
|
|
17
|
+
return Result.succeed(Option.none());
|
|
18
|
+
const lines = base.split(`
|
|
19
|
+
`), normal = lines.map(normalizeLine), starts = Array.from({ length: Math.max(0, lines.length - wanted.length + 1) }, (_, at) => at).filter((at) => wanted.every((line, offset) => normal[at + offset] === line)), [start, ...others] = starts;
|
|
20
|
+
if (start === void 0)
|
|
21
|
+
return Result.succeed(Option.none());
|
|
22
|
+
if (others.length > 0)
|
|
23
|
+
return Result.fail(failure("AmbiguousMatch", `${label(index, count)} matches ${starts.length} places once whitespace and quotes are normalised; add surrounding lines so it matches exactly once`));
|
|
24
|
+
const lineStart = (line) => lines.slice(0, line).reduce((total, text) => total + text.length + 1, 0), after = start + wanted.length, lastLine = lines[after - 1] ?? "", end = withBreak ? Math.min(base.length, lineStart(after)) : lineStart(after) - 1 - (lastLine.endsWith("\r") ? 1 : 0);
|
|
25
|
+
return Result.succeed(Option.some({ index, start: lineStart(start), end, newText: edit.newText, normalized: !0 }));
|
|
26
|
+
}, spanOf = (base, edit, index, count, fuzzy) => {
|
|
27
|
+
if (edit.oldText.length === 0)
|
|
28
|
+
return Result.fail(failure("EmptyOldText", `${label(index, count)} is empty; to create a file or replace all of it, use write`));
|
|
29
|
+
return Result.flatMap(exactSpan(base, edit, index, count), Option.match({
|
|
30
|
+
onSome: Result.succeed,
|
|
31
|
+
onNone: () => Result.flatMap(fuzzy ? normalizedSpan(base, edit, index, count) : Result.succeed(Option.none()), Option.match({
|
|
32
|
+
onSome: Result.succeed,
|
|
33
|
+
onNone: () => Result.fail(failure("NoMatch", `${label(index, count)} was not found in the file; read it again and copy the text exactly, whitespace included`))
|
|
34
|
+
}))
|
|
35
|
+
}));
|
|
36
|
+
};
|
|
37
|
+
export const applyTextEdits = (original, edits, options) => {
|
|
38
|
+
const ending = lineEndingOf(original), plain = (text) => ending === "mixed" ? text : text.replaceAll(`\r
|
|
39
|
+
`, `
|
|
40
|
+
`), base = plain(original), found = Result.all(edits.map((edit, index) => spanOf(base, { oldText: plain(edit.oldText), newText: plain(edit.newText) }, index, edits.length, options.fuzzy)));
|
|
41
|
+
return Result.flatMap(found, (spans) => {
|
|
42
|
+
const ordered = [...spans].sort((left, right) => left.start - right.start), clash = ordered.findIndex((span, at) => at > 0 && span.start < (ordered[at - 1]?.end ?? 0));
|
|
43
|
+
if (clash > 0) {
|
|
44
|
+
const [first, second] = [ordered[clash - 1]?.index ?? 0, ordered[clash]?.index ?? 0].sort((left, right) => left - right);
|
|
45
|
+
return Result.fail(failure("OverlappingEdits", `edits ${(first ?? 0) + 1} and ${(second ?? 0) + 1} change overlapping text; merge them into one edit`));
|
|
46
|
+
}
|
|
47
|
+
const built = ordered.reduce((state, span) => ({ text: state.text + base.slice(state.at, span.start) + span.newText, at: span.end }), { text: "", at: 0 }), after = built.text + base.slice(built.at);
|
|
48
|
+
if (after === base)
|
|
49
|
+
return Result.fail(failure("NoChange", "the edits leave the file as it is"));
|
|
50
|
+
const firstStart = ordered[0]?.start ?? 0;
|
|
51
|
+
return Result.succeed({
|
|
52
|
+
text: ending === "crlf" ? after.replaceAll(`
|
|
53
|
+
`, `\r
|
|
54
|
+
`) : after,
|
|
55
|
+
before: base,
|
|
56
|
+
after,
|
|
57
|
+
replaced: ordered.length,
|
|
58
|
+
normalized: ordered.some((span) => span.normalized),
|
|
59
|
+
firstChangedLine: base.slice(0, firstStart).split(`
|
|
60
|
+
`).length
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}, unifiedDiff = (path, before, after, maxChars) => {
|
|
64
|
+
const diff = TextDiff.unified(TextDiff.lines(before, after, { maxEdits: 1e4 }), { fromFile: path, toFile: path });
|
|
65
|
+
return diff.length <= maxChars ? diff : `${diff.slice(0, maxChars)}
|
|
66
|
+
[diff cut at ${maxChars} characters]
|
|
67
|
+
`;
|
|
68
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
export const LineEnding = Schema.Literals(["lf", "crlf", "mixed"]), EditPlan = Schema.Struct({
|
|
3
|
+
text: Schema.String,
|
|
4
|
+
before: Schema.String,
|
|
5
|
+
after: Schema.String,
|
|
6
|
+
replaced: Schema.Int,
|
|
7
|
+
normalized: Schema.Boolean,
|
|
8
|
+
firstChangedLine: Schema.Int
|
|
9
|
+
}), EditSpan = Schema.Struct({
|
|
10
|
+
index: Schema.Int,
|
|
11
|
+
start: Schema.Int,
|
|
12
|
+
end: Schema.Int,
|
|
13
|
+
newText: Schema.String,
|
|
14
|
+
normalized: Schema.Boolean
|
|
15
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/** One line of a file's search result: its 1-based number, its text (cut to the line width), and whether it matched or is context. */
|
|
3
|
+
export declare const SearchLine: Schema.Struct<{
|
|
4
|
+
readonly line: Schema.Int;
|
|
5
|
+
readonly text: Schema.String;
|
|
6
|
+
readonly match: Schema.Boolean;
|
|
7
|
+
}>;
|
|
8
|
+
export type SearchLine = typeof SearchLine.Type;
|
|
9
|
+
/**
|
|
10
|
+
* One file searched: the lines to show (matches and their context, in
|
|
11
|
+
* order), how many lines matched in what was searched, and whether the
|
|
12
|
+
* search stopped at its cap with more of the file left.
|
|
13
|
+
*/
|
|
14
|
+
export declare const FileSearch: Schema.Struct<{
|
|
15
|
+
readonly lines: Schema.$Array<Schema.Struct<{
|
|
16
|
+
readonly line: Schema.Int;
|
|
17
|
+
readonly text: Schema.String;
|
|
18
|
+
readonly match: Schema.Boolean;
|
|
19
|
+
}>>;
|
|
20
|
+
readonly matches: Schema.Int;
|
|
21
|
+
readonly capped: Schema.Boolean;
|
|
22
|
+
}>;
|
|
23
|
+
export type FileSearch = typeof FileSearch.Type;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Result } from "effect";
|
|
2
|
+
import type { Failure } from "@xandreed/core";
|
|
3
|
+
import type { FileSearch } from "./text-search.entity.js";
|
|
4
|
+
/**
|
|
5
|
+
* Whether a regular expression (one that compiles, without the `u` or `v`
|
|
6
|
+
* flag) repeats without bound a group that itself holds an unbounded
|
|
7
|
+
* repetition: `(a+)+`, `(x*)*`, `(?:\w+\s?)*`, `((ab)+)+`. Such patterns
|
|
8
|
+
* backtrack exponentially on some lines. Repeating a group a bounded
|
|
9
|
+
* number of times (`(\d+\.){3}`) is not counted.
|
|
10
|
+
*/
|
|
11
|
+
export declare const repeatsRepetition: (source: string) => boolean;
|
|
12
|
+
/**
|
|
13
|
+
* The pattern, refused (`InvalidPattern`) when it holds a line break: `grep`
|
|
14
|
+
* matches one line at a time, so the builtin engine would find nothing and
|
|
15
|
+
* ripgrep, which reads its patterns one per line from stdin, would search
|
|
16
|
+
* for each line of it on its own. Both engines refuse it alike.
|
|
17
|
+
*/
|
|
18
|
+
export declare const singleLinePattern: (pattern: string) => Result.Result<string, Failure>;
|
|
19
|
+
/**
|
|
20
|
+
* The expression `grep` matches with: the pattern as a JavaScript regular
|
|
21
|
+
* expression, or, with `literal`, as plain text; `ignoreCase` ignores
|
|
22
|
+
* case. Fails with `InvalidPattern` when the pattern does not compile, or
|
|
23
|
+
* when it repeats a repeating group (`repeatsRepetition`): matching runs
|
|
24
|
+
* in the host's process and cannot be interrupted, and such a pattern can
|
|
25
|
+
* take exponential time on one line.
|
|
26
|
+
*/
|
|
27
|
+
export declare const compilePattern: (pattern: string, options: {
|
|
28
|
+
readonly literal: boolean;
|
|
29
|
+
readonly ignoreCase: boolean;
|
|
30
|
+
}) => Result.Result<RegExp, Failure>;
|
|
31
|
+
/**
|
|
32
|
+
* Search a file's text line by line. Each line is cut to `lineChars`
|
|
33
|
+
* characters first and only the cut line is matched (a match past the cut
|
|
34
|
+
* is not found), so how long a line may take is bounded by the cut, not by
|
|
35
|
+
* the line. That bound holds for patterns that match in linear time: the
|
|
36
|
+
* expression runs synchronously in the host's process, and a backtracking
|
|
37
|
+
* one can still take long on a cut line. `compilePattern` refuses nested
|
|
38
|
+
* repetition, the usual exponential form, but not every slow pattern
|
|
39
|
+
* (overlapping alternatives such as `(a|a)*`). At most `cap` matching lines
|
|
40
|
+
* are taken, each with up to `context` lines before and after it;
|
|
41
|
+
* overlapping context is shown once, in line order.
|
|
42
|
+
*/
|
|
43
|
+
export declare const searchText: (text: string, pattern: RegExp, options: {
|
|
44
|
+
readonly lineChars: number;
|
|
45
|
+
readonly context: number;
|
|
46
|
+
readonly cap: number;
|
|
47
|
+
}) => FileSearch;
|
|
48
|
+
/**
|
|
49
|
+
* The model's view of matches: `path:line:text` for a matching line,
|
|
50
|
+
* `path-line-text` for context, and, when there is context, `--` between
|
|
51
|
+
* runs of lines that are not adjacent (or not in one file).
|
|
52
|
+
*/
|
|
53
|
+
export declare const renderSearchLines: (lines: ReadonlyArray<{
|
|
54
|
+
readonly path: string;
|
|
55
|
+
readonly line: number;
|
|
56
|
+
readonly text: string;
|
|
57
|
+
readonly match: boolean;
|
|
58
|
+
}>, withContext: boolean) => ReadonlyArray<string>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Result } from "effect";
|
|
2
|
+
import { clipChars, splitLines } from "./text-window.entity.functions.js";
|
|
3
|
+
const failure = (error, message) => ({ error, message }), tokenPattern = /\\[\s\S]|\[(?:\\[\s\S]|[^\]\\])*\]|\((?:\?(?::|=|!|<=|<!|<[A-Za-z_$][\w$]*>))?|\)|\{\d+(?:,\d*)?\}\??|[*+?]\??|[\s\S]/g, unbounded = (token) => token.startsWith("*") || token.startsWith("+") || /^\{\d+,\}/.test(token), isQuantifier = (token) => /^(?:[*+?]|\{\d+(?:,\d*)?\})\??$/.test(token);
|
|
4
|
+
export const repeatsRepetition = (source) => (source.match(tokenPattern) ?? []).reduce((scan, token) => {
|
|
5
|
+
if (token.startsWith("("))
|
|
6
|
+
return { groups: [...scan.groups, !1], closedRepeating: !1, nested: scan.nested };
|
|
7
|
+
if (token === ")") {
|
|
8
|
+
const inner = scan.groups.at(-1) ?? !1, outer = scan.groups.slice(0, -1);
|
|
9
|
+
return { groups: outer.length === 0 ? [inner] : [...outer.slice(0, -1), (outer.at(-1) ?? !1) || inner], closedRepeating: inner, nested: scan.nested };
|
|
10
|
+
}
|
|
11
|
+
if (isQuantifier(token) && unbounded(token))
|
|
12
|
+
return { groups: [...scan.groups.slice(0, -1), !0], closedRepeating: !1, nested: scan.nested || scan.closedRepeating };
|
|
13
|
+
return { ...scan, closedRepeating: !1 };
|
|
14
|
+
}, { groups: [!1], closedRepeating: !1, nested: !1 }).nested, singleLinePattern = (pattern) => /[\r\n]/.test(pattern) ? Result.fail(failure("InvalidPattern", "the pattern contains a line break, but grep matches one line at a time; search for a single line of the text instead")) : Result.succeed(pattern), compilePattern = (pattern, options) => {
|
|
15
|
+
const source = options.literal ? pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : pattern, compiled = Result.try({
|
|
16
|
+
try: () => new RegExp(source, options.ignoreCase ? "i" : ""),
|
|
17
|
+
catch: (error) => failure("InvalidPattern", `${pattern} is not a valid regular expression; the engine says: ${error instanceof Error ? error.message : String(error)}. Pass literal: true to search for it as text`)
|
|
18
|
+
});
|
|
19
|
+
return Result.flatMap(compiled, (regex) => !options.literal && repeatsRepetition(source) ? Result.fail(failure("InvalidPattern", `${pattern} repeats a group that itself repeats (as in (a+)+), which can take exponential time on some lines; rewrite it without the nested repetition, or search with bash (grep -E, rg)`)) : Result.succeed(regex));
|
|
20
|
+
}, searchText = (text, pattern, options) => {
|
|
21
|
+
const lines = splitLines(text).map((line) => clipChars(line, options.lineChars).text), hits = lines.reduce((state, line, at) => state.capped ? state : !pattern.test(line) ? state : state.found.length >= options.cap ? { found: state.found, capped: !0 } : { found: [...state.found, at], capped: !1 }, { found: [], capped: !1 }), shown = new Set(hits.found.flatMap((at) => Array.from({ length: options.context * 2 + 1 }, (_, offset) => at - options.context + offset).filter((line) => line >= 0 && line < lines.length))), matched = new Set(hits.found);
|
|
22
|
+
return {
|
|
23
|
+
lines: [...shown].sort((left, right) => left - right).map((at) => ({ line: at + 1, text: lines[at] ?? "", match: matched.has(at) })),
|
|
24
|
+
matches: hits.found.length,
|
|
25
|
+
capped: hits.capped
|
|
26
|
+
};
|
|
27
|
+
}, renderSearchLines = (lines, withContext) => lines.flatMap((entry, at) => {
|
|
28
|
+
const previous = lines[at - 1];
|
|
29
|
+
return [...withContext && previous !== void 0 && (previous.path !== entry.path || previous.line + 1 !== entry.line) ? ["--"] : [], `${entry.path}${entry.match ? ":" : "-"}${entry.line}${entry.match ? ":" : "-"}${entry.text}`];
|
|
30
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
export const SearchLine = Schema.Struct({
|
|
3
|
+
line: Schema.Int,
|
|
4
|
+
text: Schema.String,
|
|
5
|
+
match: Schema.Boolean
|
|
6
|
+
}), FileSearch = Schema.Struct({
|
|
7
|
+
lines: Schema.Array(SearchLine),
|
|
8
|
+
matches: Schema.Int,
|
|
9
|
+
capped: Schema.Boolean
|
|
10
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/**
|
|
3
|
+
* Lines `lineWindow` picked from a text: `lines` are lines `firstLine` to
|
|
4
|
+
* `lastLine` (1-based) of `totalLines`; `next` is the line to continue from
|
|
5
|
+
* when some are left; `clipped` when the first line alone was longer than
|
|
6
|
+
* the window's bytes and only its start is in `lines`.
|
|
7
|
+
*/
|
|
8
|
+
export declare const LineWindow: Schema.Struct<{
|
|
9
|
+
readonly lines: Schema.$Array<Schema.String>;
|
|
10
|
+
readonly firstLine: Schema.Int;
|
|
11
|
+
readonly lastLine: Schema.Int;
|
|
12
|
+
readonly totalLines: Schema.Int;
|
|
13
|
+
readonly next: Schema.Option<Schema.Int>;
|
|
14
|
+
readonly clipped: Schema.Boolean;
|
|
15
|
+
}>;
|
|
16
|
+
export type LineWindow = typeof LineWindow.Type;
|
|
17
|
+
/** The lines `headLines` keeps from the start of a list, and how many it left out. */
|
|
18
|
+
export declare const HeadLines: Schema.Struct<{
|
|
19
|
+
readonly lines: Schema.$Array<Schema.String>;
|
|
20
|
+
readonly omitted: Schema.Int;
|
|
21
|
+
}>;
|
|
22
|
+
export type HeadLines = typeof HeadLines.Type;
|
|
23
|
+
/**
|
|
24
|
+
* The end of a command's output `tailText` keeps: `text` is its last whole
|
|
25
|
+
* lines within the limits (or, when the last line alone is longer than the
|
|
26
|
+
* bytes allowed, the end of that line); `totalLines` counts the lines of the
|
|
27
|
+
* whole output; `truncated` when anything was left out.
|
|
28
|
+
*/
|
|
29
|
+
export declare const OutputTail: Schema.Struct<{
|
|
30
|
+
readonly text: Schema.String;
|
|
31
|
+
readonly shownLines: Schema.Int;
|
|
32
|
+
readonly totalLines: Schema.Int;
|
|
33
|
+
readonly truncated: Schema.Boolean;
|
|
34
|
+
}>;
|
|
35
|
+
export type OutputTail = typeof OutputTail.Type;
|
|
36
|
+
/**
|
|
37
|
+
* A file's bytes as text: the BOM, when there was one, removed and
|
|
38
|
+
* remembered; `lossy` when the bytes were not valid UTF-8, so the sequences
|
|
39
|
+
* that were not became U+FFFD and writing the text back would change them.
|
|
40
|
+
*/
|
|
41
|
+
export declare const DecodedText: Schema.Struct<{
|
|
42
|
+
readonly text: Schema.String;
|
|
43
|
+
readonly bom: Schema.Boolean;
|
|
44
|
+
readonly lossy: Schema.Boolean;
|
|
45
|
+
}>;
|
|
46
|
+
export type DecodedText = typeof DecodedText.Type;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Option, Result } from "effect";
|
|
2
|
+
import type { DecodedText, HeadLines, LineWindow, OutputTail } from "./text-window.entity.js";
|
|
3
|
+
/** How many bytes `text` takes as UTF-8. */
|
|
4
|
+
export declare const utf8Length: (text: string) => number;
|
|
5
|
+
/** The start of `text`, at most `maxBytes` of UTF-8, never cutting a character in two. */
|
|
6
|
+
export declare const clipHead: (text: string, maxBytes: number) => string;
|
|
7
|
+
/** The end of `text`, at most `maxBytes` of UTF-8, never cutting a character in two. */
|
|
8
|
+
export declare const clipTail: (text: string, maxBytes: number) => string;
|
|
9
|
+
/** The start of a line, at most `maxChars` UTF-16 code units, never splitting a surrogate pair; and whether anything was cut. */
|
|
10
|
+
export declare const clipChars: (line: string, maxChars: number) => {
|
|
11
|
+
readonly text: string;
|
|
12
|
+
readonly clipped: boolean;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* A text's lines: split at `\n`, with a `\r` before it dropped. A final
|
|
16
|
+
* newline ends the last line rather than starting an empty one, so `""` has
|
|
17
|
+
* no lines and `"a\n"` has one.
|
|
18
|
+
*/
|
|
19
|
+
export declare const splitLines: (text: string) => ReadonlyArray<string>;
|
|
20
|
+
/**
|
|
21
|
+
* The lines `read` shows: from line `offset` (1-based), at most `limit` of
|
|
22
|
+
* them and at most `maxBytes` of UTF-8, whole lines only, except that a
|
|
23
|
+
* first line longer than `maxBytes` is shown cut (`clipped`) rather than
|
|
24
|
+
* not at all. An empty text has an empty window at offset 1; an offset past
|
|
25
|
+
* the last line fails with the number of lines there are.
|
|
26
|
+
*/
|
|
27
|
+
export declare const lineWindow: (text: string, options: {
|
|
28
|
+
readonly offset: number;
|
|
29
|
+
readonly limit: number;
|
|
30
|
+
readonly maxBytes: number;
|
|
31
|
+
}) => Result.Result<LineWindow, number>;
|
|
32
|
+
/** The first lines of a list that fit `maxLines` and `maxBytes` (a first line longer than `maxBytes` is cut), and how many were left out. */
|
|
33
|
+
export declare const headLines: (lines: ReadonlyArray<string>, maxLines: number, maxBytes: number) => HeadLines;
|
|
34
|
+
/**
|
|
35
|
+
* The end of a command's output: its last whole lines within `maxLines` and
|
|
36
|
+
* `maxBytes`, or, when the last line alone is longer than `maxBytes`, that
|
|
37
|
+
* line's end.
|
|
38
|
+
*/
|
|
39
|
+
export declare const tailText: (text: string, maxLines: number, maxBytes: number) => OutputTail;
|
|
40
|
+
/** Whether bytes look binary: a NUL byte in the first 8 KB. */
|
|
41
|
+
export declare const looksBinary: (bytes: Uint8Array) => boolean;
|
|
42
|
+
/**
|
|
43
|
+
* A file's bytes as UTF-8 text, its BOM removed and remembered. Bytes that
|
|
44
|
+
* are not valid UTF-8 (Latin-1, Windows-1252, Shift-JIS) are decoded all
|
|
45
|
+
* the same, their invalid sequences as U+FFFD, and the result says `lossy`:
|
|
46
|
+
* fit to show or search, not to write back.
|
|
47
|
+
*/
|
|
48
|
+
export declare const decodeText: (bytes: Uint8Array) => DecodedText;
|
|
49
|
+
/** Text to write back: with its BOM again when it had one. */
|
|
50
|
+
export declare const encodeText: (decoded: Pick<DecodedText, "text" | "bom">) => string;
|
|
51
|
+
/** The media type a file name's extension suggests, for binary files the tools do not open. */
|
|
52
|
+
export declare const mediaTypeOf: (name: string) => Option.Option<string>;
|
|
53
|
+
/** A byte count for people: `812 B`, `50 KB`, `1.5 MB`. */
|
|
54
|
+
export declare const formatBytes: (bytes: number) => string;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Option, Result } from "effect";
|
|
2
|
+
const encoder = new TextEncoder, decoder = new TextDecoder("utf-8", { ignoreBOM: !0 }), strictDecoder = new TextDecoder("utf-8", { ignoreBOM: !0, fatal: !0 });
|
|
3
|
+
export const utf8Length = (text) => encoder.encode(text).length;
|
|
4
|
+
const isContinuation = (bytes, at) => ((bytes[at] ?? 0) & 192) === 128, startAtOrBefore = (bytes, at) => at <= 0 ? 0 : at >= bytes.length ? bytes.length : isContinuation(bytes, at) ? startAtOrBefore(bytes, at - 1) : at, startAtOrAfter = (bytes, at) => at >= bytes.length ? bytes.length : at > 0 && isContinuation(bytes, at) ? startAtOrAfter(bytes, at + 1) : Math.max(0, at);
|
|
5
|
+
export const clipHead = (text, maxBytes) => {
|
|
6
|
+
const bytes = encoder.encode(text);
|
|
7
|
+
return bytes.length <= maxBytes ? text : decoder.decode(bytes.subarray(0, startAtOrBefore(bytes, maxBytes)));
|
|
8
|
+
}, clipTail = (text, maxBytes) => {
|
|
9
|
+
const bytes = encoder.encode(text);
|
|
10
|
+
return bytes.length <= maxBytes ? text : decoder.decode(bytes.subarray(startAtOrAfter(bytes, bytes.length - maxBytes)));
|
|
11
|
+
}, clipChars = (line, maxChars) => {
|
|
12
|
+
if (line.length <= maxChars)
|
|
13
|
+
return { text: line, clipped: !1 };
|
|
14
|
+
const last = line.charCodeAt(maxChars - 1);
|
|
15
|
+
return { text: line.slice(0, last >= 55296 && last <= 56319 ? maxChars - 1 : maxChars), clipped: !0 };
|
|
16
|
+
}, splitLines = (text) => {
|
|
17
|
+
if (text.length === 0)
|
|
18
|
+
return [];
|
|
19
|
+
const parts = text.split(`
|
|
20
|
+
`);
|
|
21
|
+
return (parts.at(-1) === "" ? parts.slice(0, -1) : parts).map((line) => line.endsWith("\r") ? line.slice(0, -1) : line);
|
|
22
|
+
};
|
|
23
|
+
const fitting = (lines, maxBytes) => {
|
|
24
|
+
const over = lines.reduce((sums, line) => {
|
|
25
|
+
sums.push(sums.length === 0 ? utf8Length(line) : (sums.at(-1) ?? 0) + 1 + utf8Length(line));
|
|
26
|
+
return sums;
|
|
27
|
+
}, []).findIndex((total) => total > maxBytes);
|
|
28
|
+
return over < 0 ? lines.length : over;
|
|
29
|
+
};
|
|
30
|
+
export const lineWindow = (text, options) => {
|
|
31
|
+
const all = splitLines(text), start = options.offset - 1;
|
|
32
|
+
if (all.length === 0 && start === 0)
|
|
33
|
+
return Result.succeed({ lines: [], firstLine: 0, lastLine: 0, totalLines: 0, next: Option.none(), clipped: !1 });
|
|
34
|
+
if (start >= all.length)
|
|
35
|
+
return Result.fail(all.length);
|
|
36
|
+
const candidate = all.slice(start, start + options.limit), count = fitting(candidate, options.maxBytes), clipped = count === 0, lines = clipped ? [clipHead(candidate[0] ?? "", options.maxBytes)] : candidate.slice(0, count), lastLine = start + lines.length;
|
|
37
|
+
return Result.succeed({
|
|
38
|
+
lines,
|
|
39
|
+
firstLine: options.offset,
|
|
40
|
+
lastLine,
|
|
41
|
+
totalLines: all.length,
|
|
42
|
+
next: lastLine < all.length ? Option.some(lastLine + 1) : Option.none(),
|
|
43
|
+
clipped
|
|
44
|
+
});
|
|
45
|
+
}, headLines = (lines, maxLines, maxBytes) => {
|
|
46
|
+
const capped = lines.slice(0, maxLines), count = fitting(capped, maxBytes), kept = count === 0 && capped.length > 0 ? [clipHead(capped[0] ?? "", maxBytes)] : capped.slice(0, count);
|
|
47
|
+
return { lines: kept, omitted: lines.length - kept.length };
|
|
48
|
+
}, tailText = (text, maxLines, maxBytes) => {
|
|
49
|
+
const all = splitLines(text), capped = all.slice(Math.max(0, all.length - maxLines)), count = fitting([...capped].reverse(), maxBytes), kept = count === 0 && capped.length > 0 ? [clipTail(capped.at(-1) ?? "", maxBytes)] : capped.slice(capped.length - count);
|
|
50
|
+
return { text: kept.join(`
|
|
51
|
+
`), shownLines: kept.length, totalLines: all.length, truncated: kept.length < all.length || count === 0 && capped.length > 0 };
|
|
52
|
+
}, looksBinary = (bytes) => bytes.subarray(0, 8192).includes(0), decodeText = (bytes) => {
|
|
53
|
+
const bom = bytes.length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191, body = bom ? bytes.subarray(3) : bytes;
|
|
54
|
+
return Result.match(Result.try(() => strictDecoder.decode(body)), {
|
|
55
|
+
onSuccess: (text) => ({ text, bom, lossy: !1 }),
|
|
56
|
+
onFailure: () => ({ text: decoder.decode(body), bom, lossy: !0 })
|
|
57
|
+
});
|
|
58
|
+
}, encodeText = (decoded) => decoded.bom ? `\uFEFF${decoded.text}` : decoded.text;
|
|
59
|
+
const mediaTypes = new Map([
|
|
60
|
+
["png", "image/png"],
|
|
61
|
+
["jpg", "image/jpeg"],
|
|
62
|
+
["jpeg", "image/jpeg"],
|
|
63
|
+
["gif", "image/gif"],
|
|
64
|
+
["webp", "image/webp"],
|
|
65
|
+
["bmp", "image/bmp"],
|
|
66
|
+
["ico", "image/x-icon"],
|
|
67
|
+
["tif", "image/tiff"],
|
|
68
|
+
["tiff", "image/tiff"],
|
|
69
|
+
["avif", "image/avif"],
|
|
70
|
+
["heic", "image/heic"],
|
|
71
|
+
["pdf", "application/pdf"],
|
|
72
|
+
["zip", "application/zip"],
|
|
73
|
+
["gz", "application/gzip"],
|
|
74
|
+
["tar", "application/x-tar"],
|
|
75
|
+
["wasm", "application/wasm"],
|
|
76
|
+
["mp3", "audio/mpeg"],
|
|
77
|
+
["wav", "audio/wav"],
|
|
78
|
+
["mp4", "video/mp4"],
|
|
79
|
+
["webm", "video/webm"],
|
|
80
|
+
["woff", "font/woff"],
|
|
81
|
+
["woff2", "font/woff2"],
|
|
82
|
+
["ttf", "font/ttf"],
|
|
83
|
+
["otf", "font/otf"]
|
|
84
|
+
]);
|
|
85
|
+
export const mediaTypeOf = (name) => {
|
|
86
|
+
const base = name.slice(name.lastIndexOf("/") + 1), dot = base.lastIndexOf(".");
|
|
87
|
+
return dot <= 0 ? Option.none() : Option.fromNullishOr(mediaTypes.get(base.slice(dot + 1).toLowerCase()));
|
|
88
|
+
}, formatBytes = (bytes) => bytes < 1024 ? `${bytes} B` : bytes < 1048576 ? `${Number.isInteger(bytes / 1024) ? bytes / 1024 : (bytes / 1024).toFixed(1)} KB` : `${Number.isInteger(bytes / 1048576) ? bytes / 1048576 : (bytes / 1048576).toFixed(1)} MB`;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
export const LineWindow = Schema.Struct({
|
|
3
|
+
lines: Schema.Array(Schema.String),
|
|
4
|
+
firstLine: Schema.Int,
|
|
5
|
+
lastLine: Schema.Int,
|
|
6
|
+
totalLines: Schema.Int,
|
|
7
|
+
next: Schema.Option(Schema.Int),
|
|
8
|
+
clipped: Schema.Boolean
|
|
9
|
+
}), HeadLines = Schema.Struct({
|
|
10
|
+
lines: Schema.Array(Schema.String),
|
|
11
|
+
omitted: Schema.Int
|
|
12
|
+
}), OutputTail = Schema.Struct({
|
|
13
|
+
text: Schema.String,
|
|
14
|
+
shownLines: Schema.Int,
|
|
15
|
+
totalLines: Schema.Int,
|
|
16
|
+
truncated: Schema.Boolean
|
|
17
|
+
}), DecodedText = Schema.Struct({
|
|
18
|
+
text: Schema.String,
|
|
19
|
+
bom: Schema.Boolean,
|
|
20
|
+
lossy: Schema.Boolean
|
|
21
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/** Whether the file tools may only read a root, or may also change it. */
|
|
3
|
+
export declare const WorkspaceAccessMode: Schema.Literals<readonly ["read", "write"]>;
|
|
4
|
+
export type WorkspaceAccessMode = typeof WorkspaceAccessMode.Type;
|
|
5
|
+
/** An extra file-tool root, absolute or relative to the session's workspace. */
|
|
6
|
+
export declare const WorkspaceAllowedRoot: Schema.Struct<{
|
|
7
|
+
readonly path: Schema.NonEmptyString;
|
|
8
|
+
readonly access: Schema.Literals<readonly ["read", "write"]>;
|
|
9
|
+
}>;
|
|
10
|
+
export type WorkspaceAllowedRoot = typeof WorkspaceAllowedRoot.Type;
|
|
11
|
+
/** An allowed root, resolved on the same file system as the workspace. */
|
|
12
|
+
export declare const AllowedRoot: Schema.Struct<{
|
|
13
|
+
readonly root: Schema.String;
|
|
14
|
+
readonly lexicalRoot: Schema.String;
|
|
15
|
+
readonly access: Schema.Literals<readonly ["read", "write"]>;
|
|
16
|
+
}>;
|
|
17
|
+
export type AllowedRoot = typeof AllowedRoot.Type;
|
|
18
|
+
/**
|
|
19
|
+
* A path a tool was given, resolved in the workspace: `path` is where the
|
|
20
|
+
* tool acts (absolute, every link on the way resolved, through the nearest
|
|
21
|
+
* ancestor that exists), `display` is how the model wrote it, relative to
|
|
22
|
+
* the workspace root (`.` for the root; absolute when outside it).
|
|
23
|
+
*/
|
|
24
|
+
export declare const WorkspaceTarget: Schema.Struct<{
|
|
25
|
+
readonly path: Schema.String;
|
|
26
|
+
readonly display: Schema.String;
|
|
27
|
+
}>;
|
|
28
|
+
export type WorkspaceTarget = typeof WorkspaceTarget.Type;
|
|
29
|
+
/**
|
|
30
|
+
* The workspace a call works in: its root as configured, made absolute
|
|
31
|
+
* (`lexicalRoot`), and as the file system resolves it (`root`, links
|
|
32
|
+
* resolved). Paths are resolved against `root`.
|
|
33
|
+
*/
|
|
34
|
+
export declare const WorkspaceRoot: Schema.Struct<{
|
|
35
|
+
readonly root: Schema.String;
|
|
36
|
+
readonly lexicalRoot: Schema.String;
|
|
37
|
+
readonly roots: Schema.$Array<Schema.Struct<{
|
|
38
|
+
readonly root: Schema.String;
|
|
39
|
+
readonly lexicalRoot: Schema.String;
|
|
40
|
+
readonly access: Schema.Literals<readonly ["read", "write"]>;
|
|
41
|
+
}>>;
|
|
42
|
+
}>;
|
|
43
|
+
export type WorkspaceRoot = typeof WorkspaceRoot.Type;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Effect, Option, Result } from "effect";
|
|
2
|
+
import type { Context, Path } from "effect";
|
|
3
|
+
import { FileSystem, FsError, SessionEnvironment } from "@xandreed/core";
|
|
4
|
+
import type { Failure, FsReason } from "@xandreed/core";
|
|
5
|
+
import type { WorkspaceAccessMode, WorkspaceAllowedRoot, WorkspaceRoot, WorkspaceTarget } from "./workspace-path.entity.js";
|
|
6
|
+
type Fs = Context.Service.Shape<typeof FileSystem>;
|
|
7
|
+
/** The workspace a call works in (`WorkspaceRoot`), with the file system and path functions it uses. */
|
|
8
|
+
export type WorkspaceAccess = WorkspaceRoot & {
|
|
9
|
+
readonly fs: Fs;
|
|
10
|
+
readonly path: Path.Path;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* A file-system failure as the model reads it: what went wrong with the path
|
|
14
|
+
* it gave, in words, by the failure's reason. Never the backend's message,
|
|
15
|
+
* which names host paths the model never gave.
|
|
16
|
+
*/
|
|
17
|
+
export declare const fsFailure: (display: string) => (error: FsError) => Failure;
|
|
18
|
+
/** The workspace directory cannot be opened: why, in words, never the reason's name or the backend's message (it names the host's path). */
|
|
19
|
+
export declare const workspaceMissing: (reason: FsReason) => Failure;
|
|
20
|
+
/** Whether `path` is `root` or below it. */
|
|
21
|
+
export declare const isWithin: (root: string, path: string) => boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Open the workspace of the call: `SessionEnvironment.workspace` made
|
|
24
|
+
* absolute (against the process's directory) and resolved by the file
|
|
25
|
+
* system; one that cannot be resolved fails with `WorkspaceMissing`.
|
|
26
|
+
* Extra roots are resolved against it for this call. An extra root that
|
|
27
|
+
* cannot be opened as a directory is logged and left out.
|
|
28
|
+
*/
|
|
29
|
+
export declare const openWorkspace: (path: Path.Path, options?: {
|
|
30
|
+
readonly roots: ReadonlyArray<WorkspaceAllowedRoot>;
|
|
31
|
+
readonly workspaceAccess: WorkspaceAccessMode;
|
|
32
|
+
} | undefined) => Effect.Effect<WorkspaceAccess, {
|
|
33
|
+
readonly error: string;
|
|
34
|
+
readonly message: string;
|
|
35
|
+
}, FileSystem | SessionEnvironment>;
|
|
36
|
+
/** Whether a real path is inside any root the file tools may read. */
|
|
37
|
+
export declare const withinAnyRoot: (access: WorkspaceAccess, path: string) => boolean;
|
|
38
|
+
/** How a path appears to the model: relative to the root (`.` for the root itself), or absolute outside it. */
|
|
39
|
+
export declare const displayOf: (access: WorkspaceAccess, absolute: string) => string;
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a path a tool was given: relative to the workspace root, or
|
|
42
|
+
* absolute. With `confine`, a path written outside every allowed root is refused
|
|
43
|
+
* before anything is read, and so is one whose real location (links
|
|
44
|
+
* resolved, through the nearest existing ancestor) is outside all roots
|
|
45
|
+
* (`OutsideWorkspace`). A write into a read root fails with `ReadOnly`;
|
|
46
|
+
* the most specific real root decides access. With confinement off these
|
|
47
|
+
* restrictions are disabled. This is a guardrail against mistakes, not a
|
|
48
|
+
* sandbox: what a path names can change between this check and its use.
|
|
49
|
+
*/
|
|
50
|
+
export declare const resolveTarget: (access: WorkspaceAccess, input: string, options: {
|
|
51
|
+
readonly confine: boolean;
|
|
52
|
+
readonly intent?: WorkspaceAccessMode;
|
|
53
|
+
}) => Effect.Effect<{
|
|
54
|
+
readonly path: string;
|
|
55
|
+
readonly display: string;
|
|
56
|
+
}, {
|
|
57
|
+
readonly error: string;
|
|
58
|
+
readonly message: string;
|
|
59
|
+
}, never>;
|
|
60
|
+
/** A compiled `protect` glob: the pattern, and whether a path relative to the root matches it. */
|
|
61
|
+
export interface ProtectRule {
|
|
62
|
+
readonly glob: string;
|
|
63
|
+
readonly matches: (relative: string) => boolean;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Compile `protect` globs (dot entries included); fails with the first
|
|
67
|
+
* pattern that does not compile. A rule also matches a path that differs
|
|
68
|
+
* from it only in letter case, so a case-insensitive file system cannot
|
|
69
|
+
* reach a protected file (`.GIT/config`) through another spelling.
|
|
70
|
+
*/
|
|
71
|
+
export declare const compileProtect: (globs: ReadonlyArray<string>) => Result.Result<ReadonlyArray<ProtectRule>, string>;
|
|
72
|
+
/**
|
|
73
|
+
* The `exclude` globs as a walk from a directory must be given them, the
|
|
74
|
+
* directory written relative to the root (`""` for the root; none when it
|
|
75
|
+
* is outside the workspace). A name (no `/`) applies at any depth, so it is
|
|
76
|
+
* kept. A path is relative to the root, and the walk matches it against
|
|
77
|
+
* paths relative to where it starts: its leading segments are matched
|
|
78
|
+
* against the directory's names and what is left is kept, anchored there
|
|
79
|
+
* with `./`. A path that cannot reach below the directory, or that names
|
|
80
|
+
* the directory or one above it (the walk was asked to go there), is
|
|
81
|
+
* dropped, and so is every path when the directory is outside the
|
|
82
|
+
* workspace. A path whose braces span segments is kept as it is.
|
|
83
|
+
*/
|
|
84
|
+
export declare const excludesBelow: (excludes: ReadonlyArray<string>, directory: Option.Option<string>) => ReadonlyArray<string>;
|
|
85
|
+
/**
|
|
86
|
+
* Refuse a write to a protected path: the path as written and where it
|
|
87
|
+
* really is are both checked, relative to the root, against every rule.
|
|
88
|
+
*/
|
|
89
|
+
export declare const checkWritable: (access: WorkspaceAccess, target: WorkspaceTarget, rules: ReadonlyArray<ProtectRule>) => Effect.Effect<void, Failure>;
|
|
90
|
+
export {};
|