@narumitw/pi-files 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +102 -2
- package/dist/chunks/chunk-3GQ7VS22.ts +273 -0
- package/dist/chunks/chunk-3GQ7VS22.ts.map +7 -0
- package/dist/chunks/chunk-AXW73HDJ.ts +31 -0
- package/dist/chunks/chunk-AXW73HDJ.ts.map +7 -0
- package/dist/chunks/chunk-F5QFC255.ts +101 -0
- package/dist/chunks/chunk-F5QFC255.ts.map +7 -0
- package/dist/chunks/chunk-NFNGJETD.ts +77 -0
- package/dist/chunks/chunk-NFNGJETD.ts.map +7 -0
- package/dist/chunks/chunk-OPGTWNJI.ts +258 -0
- package/dist/chunks/chunk-OPGTWNJI.ts.map +7 -0
- package/dist/chunks/chunk-WOTDNRTG.ts +242 -0
- package/dist/chunks/chunk-WOTDNRTG.ts.map +7 -0
- package/dist/chunks/file-context-explorer-WR6GQZHF.ts +1793 -0
- package/dist/chunks/file-context-explorer-WR6GQZHF.ts.map +7 -0
- package/dist/chunks/file-context-menu-VPLHUUQ6.ts +302 -0
- package/dist/chunks/file-context-menu-VPLHUUQ6.ts.map +7 -0
- package/dist/chunks/files-menu-MES4MQ4F.ts +119 -0
- package/dist/chunks/files-menu-MES4MQ4F.ts.map +7 -0
- package/dist/chunks/files-settings-NYXQU2HV.ts +295 -0
- package/dist/chunks/files-settings-NYXQU2HV.ts.map +7 -0
- package/dist/chunks/menu-P6IBMROH.ts +876 -0
- package/dist/chunks/menu-P6IBMROH.ts.map +7 -0
- package/dist/index.ts +1789 -0
- package/dist/index.ts.map +7 -0
- package/docs/edit-scope-reference.md +142 -0
- package/docs/file-context-reference.md +91 -0
- package/docs/migration.md +105 -0
- package/docs/verification.md +99 -0
- package/package.json +58 -4
- package/src/content-search-session.ts +204 -0
- package/src/content-search-ui.ts +159 -0
- package/src/content-search.ts +170 -0
- package/src/context-service.ts +515 -0
- package/src/external-editor.ts +143 -0
- package/src/file-browser-ui.ts +128 -0
- package/src/file-browser.ts +76 -0
- package/src/file-context-explorer.ts +992 -0
- package/src/file-context-menu.ts +360 -0
- package/src/file-context-preview-ui.ts +354 -0
- package/src/file-context-settings.ts +301 -0
- package/src/file-context.ts +321 -0
- package/src/file-search.ts +246 -0
- package/src/files-menu.ts +125 -0
- package/src/files-settings.ts +314 -0
- package/src/files-widget.ts +65 -0
- package/src/files.ts +148 -0
- package/src/git-context.ts +485 -0
- package/src/index.ts +1 -0
- package/src/scope/contracts.ts +243 -0
- package/src/scope/display.ts +27 -0
- package/src/scope/edit-scope.ts +435 -0
- package/src/scope/files.ts +118 -0
- package/src/scope/input.ts +207 -0
- package/src/scope/menu.ts +277 -0
- package/src/scope/paths.ts +142 -0
- package/src/scope/picker-keys.ts +154 -0
- package/src/scope/picker.ts +484 -0
- package/src/scope/policy.ts +55 -0
- package/src/scope/settings.ts +138 -0
- package/src/scope/state.ts +91 -0
- package/src/scope/ui.ts +106 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# @narumitw/pi-files
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Merge File Context and Edit Scope into one independently installable extension with a `/files` manager, common settings, and combined status. Keep context selection and edit permission independent.
|
|
8
|
+
- Preserve the dedicated compatibility commands, context workflows, native-tool guard, settings filenames, branch-owned scope entries, original entry IDs, and historical model notices.
|
|
9
|
+
- Archive both predecessor packages in the repository and document migration without co-loading. This release intent does not authorize publication or npm deprecation notices.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Narumi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,103 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 🗂️ pi-files — File Context and Edit Restrictions
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@narumitw/pi-files)
|
|
4
|
+
[](https://github.com/earendil-works/pi)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Browse files, attach exact snapshots to the next prompt, and manage allowed edit targets through `/files`. Context selection and edit permission are independent: quoting never authorizes a file, and authorization never loads its contents.
|
|
8
|
+
|
|
9
|
+
## ✨ Features
|
|
10
|
+
|
|
11
|
+
- Project explorer with file-name and content search, exact-line previews, Git history, diffs, and provenance.
|
|
12
|
+
- Immutable context snapshots with review and removal before submission, plus whole-file references.
|
|
13
|
+
- Checked-file selection, explicit external targets, and new-file reservations for native edit restrictions.
|
|
14
|
+
- Branch-owned restrictions restored across reload, resume, fork, tree navigation, and compaction.
|
|
15
|
+
|
|
16
|
+
## 📦 Install
|
|
17
|
+
|
|
18
|
+
**Extensions run with Pi's full user permissions. Load only trusted code. This is an in-process guard, not an OS sandbox.** Read the [security boundary](#-security-and-privacy) before enabling restrictions.
|
|
19
|
+
|
|
20
|
+
Pi Files is not yet published to npm. A read-only registry query returned `E404` during implementation; it does not reserve the name or authorize publication. Use a local checkout. From the repository root:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install
|
|
24
|
+
npm run build
|
|
25
|
+
pi -e ./packages/pi-files
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The root build supplies Pi TUI Kit and generates `dist/index.ts`. An unbuilt checkout cannot be loaded by package directory. To register the built checkout for later sessions:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pi install ./packages/pi-files
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Before switching from File Context or Edit Scope, follow the [migration guide](docs/migration.md). Remove or disable predecessor resources in both user and project install scopes, then reload. **Do not co-load Pi Files with either predecessor.**
|
|
35
|
+
|
|
36
|
+
## 🚀 Quick start
|
|
37
|
+
|
|
38
|
+
1. Run `/files browse` or press `Ctrl+Shift+X`.
|
|
39
|
+
2. Open a file, press `Space` to anchor an exact line selection, extend it with arrows, then press `Enter` to queue the snapshot.
|
|
40
|
+
3. Review queued context through `/files`, then submit the request normally. Snapshots attach once, in selection order.
|
|
41
|
+
|
|
42
|
+
To restrict edits separately, open scope management from `/files`, check files in its dedicated picker, and explicitly apply. End the restriction with `/files off` before running tools such as tests or formatters.
|
|
43
|
+
|
|
44
|
+
## 💬 Commands
|
|
45
|
+
|
|
46
|
+
| Route | Result |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `/files` | Open the common manager for browsing, context review, edit scope, Settings, Status, and Help. |
|
|
49
|
+
| `/files browse` | Open the context explorer directly. |
|
|
50
|
+
| `/files off` | End the edit restriction without changing queued context. |
|
|
51
|
+
| `/file-context [browse\|remove]` | Retain the dedicated context manager, direct browser, and snapshot-review route. `remove` does not delete files. |
|
|
52
|
+
| `/edit-scope`, `/scope` | Retain the dedicated scope manager. |
|
|
53
|
+
| `/edit-scope off`, `/scope off` | End the same restriction. |
|
|
54
|
+
|
|
55
|
+
Interactive routes require TUI mode and reject unknown or trailing arguments. Scope routes and permission changes require an idle agent with no pending messages. RPC, print, and JSON reject interactive commands before custom UI; restored enforcement remains active in all modes.
|
|
56
|
+
|
|
57
|
+
The explorer and checked-file picker remain separate components with different selection keys. There is no combined quote-and-authorize action. Closing a screen, cancelling, or ending a run does not release an applied restriction. See the [context workflow](docs/file-context-reference.md) and [scope workflow and recovery](docs/edit-scope-reference.md) for interaction, mode errors, and limits.
|
|
58
|
+
|
|
59
|
+
## ⚙️ Settings
|
|
60
|
+
|
|
61
|
+
Open `/files` → **Settings** for the common Pi `SettingsList` screen. Changes save immediately; Escape does not undo a saved change. Only **Open shortcut** is blocked while context is queued because it reloads Pi. **Show hidden files** applies immediately to scope discovery without reload or permission changes. Dedicated compatibility managers keep their existing settings routes.
|
|
62
|
+
|
|
63
|
+
The two user-only stores deliberately retain their legacy basenames under `getAgentDir()` (normally `~/.pi/agent`):
|
|
64
|
+
|
|
65
|
+
| File | Setting and default | Detailed reference |
|
|
66
|
+
| --- | --- | --- |
|
|
67
|
+
| `pi-file-context.json` | `openShortcut: "ctrl+shift+x"`; `null` disables it | [Shortcut and editor settings](docs/file-context-reference.md#settings) |
|
|
68
|
+
| `pi-scope.json` | `showHiddenFiles: false`; boolean | [Scope settings and persistence](docs/edit-scope-reference.md#settings-and-persistence) |
|
|
69
|
+
|
|
70
|
+
These filenames are compatibility exceptions. There is no `pi-files.json`, automatic filename migration, project override, or extension-specific environment variable. Edit the files manually and run `/reload` to apply changes. Defaults do not create files; invalid files are not overwritten, and failed saves preserve the prior effective value. See the linked references for accepted values, ordering, and failure recovery.
|
|
71
|
+
|
|
72
|
+
## 🔒 Security and privacy
|
|
73
|
+
|
|
74
|
+
The supported guard boundary is **unmodified stock Pi CLI tools with trusted extensions**, not arbitrary SDK `baseToolsOverride` or custom `prepareLoadout` implementations.
|
|
75
|
+
|
|
76
|
+
While restricted, stock readers remain available for other files. Stock `write` and `edit` require an exact, freshly validated canonical target. Shell tools, codemode, delegation, replacement tools, and unsupported custom tools are blocked, including tools with `readOnlyHint`. At least one stock direct writer must be active; active native `codemode.mode=only` is unsupported. The extension does not alter active tools or core settings to make a loadout acceptable.
|
|
77
|
+
|
|
78
|
+
The guard does not contain user-issued shell commands, RPC shell calls, extension code, other programs, existing background processes, or filesystem races. **The user-controlled external editor is outside this guard**, runs with user permissions, and can modify other files or access services. Historical previews remain read-only. Selected scope paths and canonical redirects enter a model notice; selected quotes send file text to the model. Inspect content and Git diffs before submission.
|
|
79
|
+
|
|
80
|
+
See [context data boundaries](docs/file-context-reference.md#security-and-data-limits) and [scope protection and recovery](docs/edit-scope-reference.md#protection-boundary) before relying on either workflow.
|
|
81
|
+
|
|
82
|
+
## 🚧 Limitations
|
|
83
|
+
|
|
84
|
+
Queued context is ephemeral and does not survive reload, session replacement, or shutdown. Submit or remove it explicitly before migration or reload. Scope persists separately; invalid, uncertain, or unsupported restored scope fails closed until repaired or explicitly ended.
|
|
85
|
+
|
|
86
|
+
Discovery does not implement `.gitignore`. The bounded context inventory is not a complete scope inventory: scope scan errors and limits fail the operation rather than authorize a partial result. Detailed discovery, path, UI, and platform limits remain in the linked behavior references. Physical-terminal and untested platform behavior is not established by deterministic tests; current evidence belongs in [verification](docs/verification.md).
|
|
87
|
+
|
|
88
|
+
## 🗂️ Package layout
|
|
89
|
+
|
|
90
|
+
- `src/index.ts` forwards to the single factory in `src/files.ts`; package-owned context and scope modules retain their separate responsibilities.
|
|
91
|
+
- `dist/index.ts` and generated chunks form the published Jiti runtime; `scripts/build-runtime.mjs` uses the repository builder and keeps Pi peers external.
|
|
92
|
+
- `docs/` contains both behavior references, migration guidance, and verification evidence.
|
|
93
|
+
- `test/` contains repository-only coverage and smoke fixtures.
|
|
94
|
+
|
|
95
|
+
Published files include `src/`, `dist/`, `docs/`, README, changelog, license, and package metadata. Tests, build scripts, and repository configuration are not shipped. Build before packing or loading the local package directory.
|
|
96
|
+
|
|
97
|
+
## 🔎 Keywords
|
|
98
|
+
|
|
99
|
+
Pi extension, file explorer, exact context, Git provenance, edit scope, file allowlist, native editing guard.
|
|
100
|
+
|
|
101
|
+
## 📄 License
|
|
102
|
+
|
|
103
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// @generated by scripts/build-runtime.mjs; do not edit.
|
|
2
|
+
// @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
|
|
3
|
+
import {
|
|
4
|
+
resolveScopeTarget
|
|
5
|
+
} from "./chunk-F5QFC255.ts";
|
|
6
|
+
import {
|
|
7
|
+
displayError,
|
|
8
|
+
displayPath
|
|
9
|
+
} from "./chunk-AXW73HDJ.ts";
|
|
10
|
+
|
|
11
|
+
// src/scope/ui.ts
|
|
12
|
+
import { stripVTControlCharacters } from "node:util";
|
|
13
|
+
import { HorizontalRule, hardWrapTerminalDocument, renderBoundedFrameLayout } from "@narumitw/pi-tui-kit";
|
|
14
|
+
function scopeFrame(tui, theme, width, title, context, content, priorityRows, focusedRow, hints = []) {
|
|
15
|
+
const rows = Number.isFinite(tui.terminal.rows) ? Math.floor(tui.terminal.rows) : 24;
|
|
16
|
+
const rule = new HorizontalRule({ ruleStyle: (line) => theme.fg("border", line) });
|
|
17
|
+
return renderBoundedFrameLayout({
|
|
18
|
+
width,
|
|
19
|
+
maxRows: Math.max(1, rows - 3),
|
|
20
|
+
rule: rule.render(width)[0] ?? "",
|
|
21
|
+
title: [theme.fg("accent", theme.bold(title))],
|
|
22
|
+
context: context.flatMap((line) => hardWrapTerminalDocument(line, width).map((part) => theme.fg("muted", part))),
|
|
23
|
+
content,
|
|
24
|
+
hints: hints.flatMap((line) => exactRows(line, width).map((part) => theme.fg("dim", part))),
|
|
25
|
+
compactHint: hints[0] ? theme.fg("dim", hints[0]) : "",
|
|
26
|
+
priorityRows,
|
|
27
|
+
focusedRow
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function exactRows(value, width) {
|
|
31
|
+
return hardWrapTerminalDocument(value, width);
|
|
32
|
+
}
|
|
33
|
+
function scopeOverlayContext(ctx) {
|
|
34
|
+
const ui = ctx.ui;
|
|
35
|
+
const custom = (factory, options) => ui.custom(
|
|
36
|
+
(tui, theme, keys, done) => {
|
|
37
|
+
const created = factory(tui, theme, keys, done);
|
|
38
|
+
return created instanceof Promise ? created.then((component) => boundedOverlay(component, tui, theme)) : boundedOverlay(created, tui, theme);
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
...options,
|
|
42
|
+
overlay: true,
|
|
43
|
+
overlayOptions: {
|
|
44
|
+
anchor: "bottom-left",
|
|
45
|
+
width: "100%",
|
|
46
|
+
maxHeight: "100%",
|
|
47
|
+
margin: 0
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
const scopedUI = new Proxy(ui, {
|
|
52
|
+
get(target, key) {
|
|
53
|
+
if (key === "custom") return custom;
|
|
54
|
+
const value = Reflect.get(target, key, target);
|
|
55
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return new Proxy(ctx, {
|
|
59
|
+
get(target, key) {
|
|
60
|
+
if (key === "ui") return scopedUI;
|
|
61
|
+
const value = Reflect.get(target, key, target);
|
|
62
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function boundedOverlay(component, tui, theme) {
|
|
67
|
+
const render = (width) => {
|
|
68
|
+
width = Number.isFinite(width) ? Math.max(0, Math.floor(width)) : 0;
|
|
69
|
+
if (width === 0) return [];
|
|
70
|
+
const lines = component.render(width);
|
|
71
|
+
const height = Math.max(1, Number.isFinite(tui.terminal.rows) ? Math.floor(tui.terminal.rows) : 24);
|
|
72
|
+
if (lines.length <= height) return lines;
|
|
73
|
+
const rule = new HorizontalRule({ ruleStyle: (line) => theme.fg("border", line) }).render(width)[0] ?? "";
|
|
74
|
+
const framed = lines[0] === rule && lines.at(-1) === rule;
|
|
75
|
+
const content = (framed ? lines.slice(1, -1) : lines).filter((line) => stripVTControlCharacters(line).trim());
|
|
76
|
+
return renderBoundedFrameLayout({ width, maxRows: height, rule, title: [], content, priorityRows: [0] }).lines;
|
|
77
|
+
};
|
|
78
|
+
return new Proxy(component, {
|
|
79
|
+
get(target, key) {
|
|
80
|
+
if (key === "render") return render;
|
|
81
|
+
const value = Reflect.get(target, key, target);
|
|
82
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
83
|
+
},
|
|
84
|
+
set(target, key, value) {
|
|
85
|
+
return Reflect.set(target, key, value, target);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/scope/input.ts
|
|
91
|
+
import { Input, matchesKey } from "@earendil-works/pi-tui";
|
|
92
|
+
import { runCustomInteraction } from "@narumitw/pi-tui-kit";
|
|
93
|
+
var PASTE_START = "\x1B[200~";
|
|
94
|
+
var PASTE_END = "\x1B[201~";
|
|
95
|
+
var ScopeInput = class {
|
|
96
|
+
input;
|
|
97
|
+
pasting = false;
|
|
98
|
+
pasteTail = "";
|
|
99
|
+
discardPaste = false;
|
|
100
|
+
constructor(placeholder = "", placeholderStyle) {
|
|
101
|
+
this.input = new Input({ placeholder, placeholderStyle });
|
|
102
|
+
}
|
|
103
|
+
get isPasting() {
|
|
104
|
+
return this.pasting;
|
|
105
|
+
}
|
|
106
|
+
handleInput(data, action, editable = true, afterPaste) {
|
|
107
|
+
if (this.pasting || data.includes(PASTE_START)) {
|
|
108
|
+
if (!this.pasting) this.discardPaste = !editable;
|
|
109
|
+
const combined = this.pasteTail + data;
|
|
110
|
+
const start = this.pasting ? 0 : combined.indexOf(PASTE_START) + PASTE_START.length;
|
|
111
|
+
const end = combined.indexOf(PASTE_END, start);
|
|
112
|
+
if (end < 0) {
|
|
113
|
+
this.pasting = true;
|
|
114
|
+
this.pasteTail = combined.slice(-5);
|
|
115
|
+
if (!this.discardPaste) this.input.handleInput(data);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const consumed = end + PASTE_END.length - this.pasteTail.length;
|
|
119
|
+
if (!this.discardPaste) this.input.handleInput(data.slice(0, consumed));
|
|
120
|
+
this.pasting = false;
|
|
121
|
+
this.discardPaste = false;
|
|
122
|
+
this.pasteTail = "";
|
|
123
|
+
const remaining = data.slice(consumed);
|
|
124
|
+
if (remaining) {
|
|
125
|
+
if (afterPaste) afterPaste(remaining);
|
|
126
|
+
else this.handleInput(remaining, action, editable);
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!action(data) && editable) this.input.handleInput(data);
|
|
131
|
+
}
|
|
132
|
+
render(width) {
|
|
133
|
+
return this.withDisplayValue(() => this.input.render(width));
|
|
134
|
+
}
|
|
135
|
+
handleMouse(event) {
|
|
136
|
+
return this.withDisplayValue(() => this.input.handleMouse(event));
|
|
137
|
+
}
|
|
138
|
+
withDisplayValue(operation) {
|
|
139
|
+
const raw = this.input.getValue();
|
|
140
|
+
const safe = raw.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, (character) => "\uFFFD".repeat(character.length));
|
|
141
|
+
this.input.setValue(safe);
|
|
142
|
+
try {
|
|
143
|
+
return operation();
|
|
144
|
+
} finally {
|
|
145
|
+
this.input.setValue(raw);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
function showScopePathInput(ctx, options) {
|
|
150
|
+
return runCustomInteraction(ctx, {
|
|
151
|
+
signal: options.signal,
|
|
152
|
+
isCurrent: options.isCurrent,
|
|
153
|
+
create: ({ tui, theme, keybindings, signal: ownerSignal, complete }) => {
|
|
154
|
+
const controller = new AbortController();
|
|
155
|
+
const signal = AbortSignal.any([controller.signal, ownerSignal]);
|
|
156
|
+
const editor = new ScopeInput();
|
|
157
|
+
let error = "";
|
|
158
|
+
let pending = Promise.resolve();
|
|
159
|
+
let submitting = false;
|
|
160
|
+
let disposed = false;
|
|
161
|
+
let closing = false;
|
|
162
|
+
let focused = false;
|
|
163
|
+
let inputRow;
|
|
164
|
+
let frameWidth = 0;
|
|
165
|
+
const current = () => {
|
|
166
|
+
if (disposed || closing || signal.aborted) return false;
|
|
167
|
+
if (options.isCurrent()) return true;
|
|
168
|
+
closing = true;
|
|
169
|
+
complete({ kind: "cancelled", reason: "close" });
|
|
170
|
+
controller.abort();
|
|
171
|
+
return false;
|
|
172
|
+
};
|
|
173
|
+
const close = (value) => {
|
|
174
|
+
if (!current()) return;
|
|
175
|
+
closing = true;
|
|
176
|
+
complete(value);
|
|
177
|
+
controller.abort();
|
|
178
|
+
};
|
|
179
|
+
const submit = () => {
|
|
180
|
+
if (!current() || submitting) return;
|
|
181
|
+
submitting = true;
|
|
182
|
+
error = "";
|
|
183
|
+
const draft = editor.input.getValue();
|
|
184
|
+
pending = (async () => {
|
|
185
|
+
try {
|
|
186
|
+
const resolved = await resolveScopeTarget(draft, options.cwd, "new", signal);
|
|
187
|
+
if (!current()) return;
|
|
188
|
+
close({ kind: "selected", target: resolved });
|
|
189
|
+
} catch (failure) {
|
|
190
|
+
if (current()) error = displayError(failure);
|
|
191
|
+
} finally {
|
|
192
|
+
submitting = false;
|
|
193
|
+
if (current()) tui.requestRender();
|
|
194
|
+
}
|
|
195
|
+
})();
|
|
196
|
+
};
|
|
197
|
+
editor.input.onSubmit = submit;
|
|
198
|
+
editor.input.onEscape = () => close({ kind: "cancelled", reason: "back" });
|
|
199
|
+
return {
|
|
200
|
+
get focused() {
|
|
201
|
+
return focused;
|
|
202
|
+
},
|
|
203
|
+
set focused(value) {
|
|
204
|
+
focused = value;
|
|
205
|
+
editor.input.focused = value;
|
|
206
|
+
},
|
|
207
|
+
render(width) {
|
|
208
|
+
inputRow = void 0;
|
|
209
|
+
frameWidth = width;
|
|
210
|
+
const safeWidth = Math.max(1, width);
|
|
211
|
+
const content = [
|
|
212
|
+
...editor.render(safeWidth),
|
|
213
|
+
...exactRows(`Draft: ${displayPath(editor.input.getValue())}`, safeWidth).map(
|
|
214
|
+
(line) => theme.fg("muted", line)
|
|
215
|
+
),
|
|
216
|
+
...error ? exactRows(error, safeWidth).map((line) => theme.fg("error", line)) : [],
|
|
217
|
+
...submitting ? [theme.fg("dim", "Checking target\u2026")] : []
|
|
218
|
+
];
|
|
219
|
+
const priorities = [0, ...error ? [content.length - 1] : []];
|
|
220
|
+
const frame = scopeFrame(
|
|
221
|
+
tui,
|
|
222
|
+
theme,
|
|
223
|
+
width,
|
|
224
|
+
"New file",
|
|
225
|
+
[
|
|
226
|
+
`Cwd: ${displayPath(options.cwd)}`,
|
|
227
|
+
"Use a cwd-relative or full path. Submit to review; no file content is created here."
|
|
228
|
+
],
|
|
229
|
+
content,
|
|
230
|
+
priorities,
|
|
231
|
+
0
|
|
232
|
+
);
|
|
233
|
+
inputRow = frame.contentRows.find((row) => row.contentIndex === 0)?.frameIndex;
|
|
234
|
+
return frame.lines;
|
|
235
|
+
},
|
|
236
|
+
handleInput(data) {
|
|
237
|
+
if (!current()) return;
|
|
238
|
+
const action = (key) => {
|
|
239
|
+
if (matchesKey(key, "ctrl+c")) close({ kind: "cancelled", reason: "close" });
|
|
240
|
+
else if (keybindings.matches(key, "tui.select.cancel")) close({ kind: "cancelled", reason: "back" });
|
|
241
|
+
else return false;
|
|
242
|
+
return true;
|
|
243
|
+
};
|
|
244
|
+
editor.handleInput(data, action, !submitting);
|
|
245
|
+
tui.requestRender();
|
|
246
|
+
},
|
|
247
|
+
handleMouse(event) {
|
|
248
|
+
if (!current() || submitting || event.width !== frameWidth || event.y !== inputRow) return;
|
|
249
|
+
return editor.handleMouse({ ...event, y: 0, height: 1 });
|
|
250
|
+
},
|
|
251
|
+
invalidate() {
|
|
252
|
+
inputRow = void 0;
|
|
253
|
+
editor.input.invalidate();
|
|
254
|
+
},
|
|
255
|
+
waitForPending: () => pending,
|
|
256
|
+
dispose() {
|
|
257
|
+
if (disposed) return;
|
|
258
|
+
disposed = true;
|
|
259
|
+
controller.abort();
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export {
|
|
267
|
+
scopeFrame,
|
|
268
|
+
exactRows,
|
|
269
|
+
scopeOverlayContext,
|
|
270
|
+
ScopeInput,
|
|
271
|
+
showScopePathInput
|
|
272
|
+
};
|
|
273
|
+
//# sourceMappingURL=chunk-3GQ7VS22.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/scope/ui.ts", "../../src/scope/input.ts"],
|
|
4
|
+
"sourcesContent": ["import { stripVTControlCharacters } from \"node:util\";\nimport type { ExtensionCommandContext, ExtensionUIContext, Theme } from \"@earendil-works/pi-coding-agent\";\nimport type { Component, TUI } from \"@earendil-works/pi-tui\";\nimport { HorizontalRule, hardWrapTerminalDocument, renderBoundedFrameLayout } from \"@narumitw/pi-tui-kit\";\n\nexport function scopeFrame(\n tui: TUI,\n theme: Theme,\n width: number,\n title: string,\n context: readonly string[],\n content: readonly string[],\n priorityRows: readonly number[],\n focusedRow?: number,\n hints: readonly string[] = [],\n) {\n // The owned overlay uses terminal bounds; three spare rows are presentation\n // space, not a prediction of the native editor dock or another widget's height.\n const rows = Number.isFinite(tui.terminal.rows) ? Math.floor(tui.terminal.rows) : 24;\n const rule = new HorizontalRule({ ruleStyle: (line) => theme.fg(\"border\", line) });\n return renderBoundedFrameLayout({\n width,\n maxRows: Math.max(1, rows - 3),\n rule: rule.render(width)[0] ?? \"\",\n title: [theme.fg(\"accent\", theme.bold(title))],\n context: context.flatMap((line) => hardWrapTerminalDocument(line, width).map((part) => theme.fg(\"muted\", part))),\n content,\n hints: hints.flatMap((line) => exactRows(line, width).map((part) => theme.fg(\"dim\", part))),\n compactHint: hints[0] ? theme.fg(\"dim\", hints[0]) : \"\",\n priorityRows,\n focusedRow,\n });\n}\n\nexport function exactRows(value: string, width: number): string[] {\n return hardWrapTerminalDocument(value, width);\n}\n\n/**\n * Pi does not expose the editor dock's allocated height to custom components.\n * A full-width bottom overlay gives every Scope screen the real terminal bounds\n * without inspecting, hiding, or changing another component's widgets or footer.\n */\nexport function scopeOverlayContext(ctx: ExtensionCommandContext): ExtensionCommandContext {\n const ui = ctx.ui;\n const custom: ExtensionUIContext[\"custom\"] = (factory, options) =>\n ui.custom(\n (tui, theme, keys, done) => {\n const created = factory(tui, theme, keys, done);\n return created instanceof Promise\n ? created.then((component) => boundedOverlay(component, tui, theme))\n : boundedOverlay(created, tui, theme);\n },\n {\n ...options,\n overlay: true,\n overlayOptions: {\n anchor: \"bottom-left\",\n width: \"100%\",\n maxHeight: \"100%\",\n margin: 0,\n },\n },\n );\n const scopedUI = new Proxy(ui, {\n get(target, key) {\n if (key === \"custom\") return custom;\n const value = Reflect.get(target, key, target);\n return typeof value === \"function\" ? value.bind(target) : value;\n },\n });\n return new Proxy(ctx, {\n get(target, key) {\n if (key === \"ui\") return scopedUI;\n const value = Reflect.get(target, key, target);\n return typeof value === \"function\" ? value.bind(target) : value;\n },\n });\n}\n\nfunction boundedOverlay<C extends Component>(component: C, tui: TUI, theme: Theme): C {\n const render = (width: number) => {\n width = Number.isFinite(width) ? Math.max(0, Math.floor(width)) : 0;\n if (width === 0) return [];\n const lines = component.render(width);\n const height = Math.max(1, Number.isFinite(tui.terminal.rows) ? Math.floor(tui.terminal.rows) : 24);\n if (lines.length <= height) return lines;\n // Scope and Kit menu frames already fit. Kit's fixed-height task loader can\n // overflow tiny terminals: remove only its frame/spacing, then retain the\n // task label through the public bounded frame without changing cancellation.\n const rule = new HorizontalRule({ ruleStyle: (line) => theme.fg(\"border\", line) }).render(width)[0] ?? \"\";\n const framed = lines[0] === rule && lines.at(-1) === rule;\n const content = (framed ? lines.slice(1, -1) : lines).filter((line) => stripVTControlCharacters(line).trim());\n return renderBoundedFrameLayout({ width, maxRows: height, rule, title: [], content, priorityRows: [0] }).lines;\n };\n return new Proxy(component, {\n get(target, key) {\n if (key === \"render\") return render;\n const value = Reflect.get(target, key, target);\n return typeof value === \"function\" ? value.bind(target) : value;\n },\n set(target, key, value) {\n return Reflect.set(target, key, value, target);\n },\n });\n}\n", "import type { ExtensionCommandContext } from \"@earendil-works/pi-coding-agent\";\nimport { Input, matchesKey, type TuiMouseEvent } from \"@earendil-works/pi-tui\";\nimport { runCustomInteraction } from \"@narumitw/pi-tui-kit\";\nimport { displayError, displayPath } from \"./display.js\";\nimport { resolveScopeTarget, type ScopeTarget } from \"./paths.js\";\nimport { exactRows, scopeFrame } from \"./ui.js\";\n\nconst PASTE_START = \"\\u001b[200~\";\nconst PASTE_END = \"\\u001b[201~\";\n\n/** Public Input editing with paste-aware screen dispatch and display-only control masking. */\nexport class ScopeInput {\n readonly input: Input;\n private pasting = false;\n private pasteTail = \"\";\n private discardPaste = false;\n\n constructor(placeholder = \"\", placeholderStyle?: (text: string) => string) {\n this.input = new Input({ placeholder, placeholderStyle });\n }\n\n get isPasting(): boolean {\n return this.pasting;\n }\n\n handleInput(\n data: string,\n action: (key: string) => boolean,\n editable = true,\n afterPaste?: (remaining: string) => void,\n ): void {\n if (this.pasting || data.includes(PASTE_START)) {\n if (!this.pasting) this.discardPaste = !editable;\n const combined = this.pasteTail + data;\n const start = this.pasting ? 0 : combined.indexOf(PASTE_START) + PASTE_START.length;\n const end = combined.indexOf(PASTE_END, start);\n if (end < 0) {\n this.pasting = true;\n this.pasteTail = combined.slice(-5);\n if (!this.discardPaste) this.input.handleInput(data);\n return;\n }\n const consumed = end + PASTE_END.length - this.pasteTail.length;\n if (!this.discardPaste) this.input.handleInput(data.slice(0, consumed));\n this.pasting = false;\n this.discardPaste = false;\n this.pasteTail = \"\";\n const remaining = data.slice(consumed);\n if (remaining) {\n if (afterPaste) afterPaste(remaining);\n else this.handleInput(remaining, action, editable);\n }\n return;\n }\n if (!action(data) && editable) this.input.handleInput(data);\n }\n\n render(width: number): string[] {\n return this.withDisplayValue(() => this.input.render(width));\n }\n\n handleMouse(event: TuiMouseEvent) {\n return this.withDisplayValue(() => this.input.handleMouse(event));\n }\n\n private withDisplayValue<T>(operation: () => T): T {\n const raw = this.input.getValue();\n // Native rendering and hit-testing must measure the same masked graphemes.\n // Equal UTF-16 length preserves cursor offsets; only the display value changes.\n const safe = raw.replace(/[\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}]/gu, (character) => \"\uFFFD\".repeat(character.length));\n this.input.setValue(safe);\n try {\n return operation();\n } finally {\n this.input.setValue(raw);\n }\n }\n}\n\ntype PathInputValue = { kind: \"selected\"; target: ScopeTarget } | { kind: \"cancelled\"; reason: \"back\" | \"close\" };\n\ninterface PathInputOptions {\n cwd: string;\n signal: AbortSignal;\n isCurrent(): boolean;\n}\n\nexport function showScopePathInput(ctx: ExtensionCommandContext, options: PathInputOptions) {\n return runCustomInteraction<PathInputValue>(ctx, {\n signal: options.signal,\n isCurrent: options.isCurrent,\n create: ({ tui, theme, keybindings, signal: ownerSignal, complete }) => {\n const controller = new AbortController();\n const signal = AbortSignal.any([controller.signal, ownerSignal]);\n const editor = new ScopeInput();\n let error = \"\";\n let pending = Promise.resolve();\n let submitting = false;\n let disposed = false;\n let closing = false;\n let focused = false;\n let inputRow: number | undefined;\n let frameWidth = 0;\n const current = () => {\n if (disposed || closing || signal.aborted) return false;\n if (options.isCurrent()) return true;\n closing = true;\n // Calling the public completion boundary with stale ownership makes\n // runCustomInteraction settle as stale and drain this component.\n complete({ kind: \"cancelled\", reason: \"close\" });\n controller.abort();\n return false;\n };\n const close = (value: PathInputValue) => {\n if (!current()) return;\n closing = true;\n complete(value);\n controller.abort();\n };\n const submit = () => {\n if (!current() || submitting) return;\n submitting = true;\n error = \"\";\n const draft = editor.input.getValue();\n pending = (async () => {\n try {\n const resolved = await resolveScopeTarget(draft, options.cwd, \"new\", signal);\n if (!current()) return;\n close({ kind: \"selected\", target: resolved });\n } catch (failure) {\n if (current()) error = displayError(failure);\n } finally {\n submitting = false;\n if (current()) tui.requestRender();\n }\n })();\n };\n editor.input.onSubmit = submit;\n editor.input.onEscape = () => close({ kind: \"cancelled\", reason: \"back\" });\n return {\n get focused() {\n return focused;\n },\n set focused(value: boolean) {\n focused = value;\n editor.input.focused = value;\n },\n render(width: number) {\n inputRow = undefined;\n frameWidth = width;\n const safeWidth = Math.max(1, width);\n const content = [\n ...editor.render(safeWidth),\n ...exactRows(`Draft: ${displayPath(editor.input.getValue())}`, safeWidth).map((line) =>\n theme.fg(\"muted\", line),\n ),\n ...(error ? exactRows(error, safeWidth).map((line) => theme.fg(\"error\", line)) : []),\n ...(submitting ? [theme.fg(\"dim\", \"Checking target\u2026\")] : []),\n ];\n const priorities = [0, ...(error ? [content.length - 1] : [])];\n // No key hints: public host listeners may consume configured keys before\n // component dispatch. No replacement shortcuts or fallback keys are added.\n const frame = scopeFrame(\n tui,\n theme,\n width,\n \"New file\",\n [\n `Cwd: ${displayPath(options.cwd)}`,\n \"Use a cwd-relative or full path. Submit to review; no file content is created here.\",\n ],\n content,\n priorities,\n 0,\n );\n inputRow = frame.contentRows.find((row) => row.contentIndex === 0)?.frameIndex;\n return frame.lines;\n },\n handleInput(data: string) {\n if (!current()) return;\n const action = (key: string) => {\n if (matchesKey(key, \"ctrl+c\")) close({ kind: \"cancelled\", reason: \"close\" });\n else if (keybindings.matches(key, \"tui.select.cancel\")) close({ kind: \"cancelled\", reason: \"back\" });\n else return false;\n return true;\n };\n editor.handleInput(data, action, !submitting);\n tui.requestRender();\n },\n handleMouse(event: TuiMouseEvent) {\n if (!current() || submitting || event.width !== frameWidth || event.y !== inputRow) return;\n return editor.handleMouse({ ...event, y: 0, height: 1 });\n },\n invalidate() {\n inputRow = undefined;\n editor.input.invalidate();\n },\n waitForPending: () => pending,\n dispose() {\n if (disposed) return;\n disposed = true;\n controller.abort();\n },\n };\n },\n });\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// @generated by scripts/build-runtime.mjs; do not edit.
|
|
2
|
+
// @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
|
|
3
|
+
|
|
4
|
+
// src/scope/display.ts
|
|
5
|
+
import { sanitizeTerminalText } from "@narumitw/pi-tui-kit/terminal-text";
|
|
6
|
+
function displayPath(path) {
|
|
7
|
+
return JSON.stringify(path).replace(
|
|
8
|
+
/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000]/gu,
|
|
9
|
+
escapeCharacter
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
function displayFilePath(path) {
|
|
13
|
+
const quoted = displayPath(path);
|
|
14
|
+
return quoted === `"${path}"` && !/^\s|\s$| {2}/u.test(path) ? path : quoted;
|
|
15
|
+
}
|
|
16
|
+
function escapeCharacter(character) {
|
|
17
|
+
return Array.from(character, (part) => {
|
|
18
|
+
const point = part.codePointAt(0) ?? 0;
|
|
19
|
+
return point > 65535 ? `\\u{${point.toString(16)}}` : `\\u${point.toString(16).padStart(4, "0")}`;
|
|
20
|
+
}).join("");
|
|
21
|
+
}
|
|
22
|
+
function displayError(error) {
|
|
23
|
+
return sanitizeTerminalText(error instanceof Error ? error.message : String(error));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
displayPath,
|
|
28
|
+
displayFilePath,
|
|
29
|
+
displayError
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=chunk-AXW73HDJ.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/scope/display.ts"],
|
|
4
|
+
"sourcesContent": ["import { sanitizeTerminalText } from \"@narumitw/pi-tui-kit/terminal-text\";\n\n// Escape controls rather than deleting bytes from a path's display identity.\n// JSON quotes also expose leading/trailing spaces and distinguish literal backslashes.\nexport function displayPath(path: string): string {\n return JSON.stringify(path).replace(\n /[\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}\\u00a0\\u1680\\u2000-\\u200a\\u202f\\u205f\\u3000]/gu,\n escapeCharacter,\n );\n}\n\n/** Keep ordinary picker labels readable without hiding ambiguous or unsafe bytes. */\nexport function displayFilePath(path: string): string {\n const quoted = displayPath(path);\n return quoted === `\"${path}\"` && !/^\\s|\\s$| {2}/u.test(path) ? path : quoted;\n}\n\nfunction escapeCharacter(character: string): string {\n return Array.from(character, (part) => {\n const point = part.codePointAt(0) ?? 0;\n return point > 0xffff ? `\\\\u{${point.toString(16)}}` : `\\\\u${point.toString(16).padStart(4, \"0\")}`;\n }).join(\"\");\n}\n\nexport function displayError(error: unknown): string {\n return sanitizeTerminalText(error instanceof Error ? error.message : String(error));\n}\n"],
|
|
5
|
+
"mappings": ";;;;AAAA,SAAS,4BAA4B;AAI9B,SAAS,YAAY,MAAsB;AAChD,SAAO,KAAK,UAAU,IAAI,EAAE;AAAA,IAC1B;AAAA,IACA;AAAA,EACF;AACF;AAGO,SAAS,gBAAgB,MAAsB;AACpD,QAAM,SAAS,YAAY,IAAI;AAC/B,SAAO,WAAW,IAAI,IAAI,OAAO,CAAC,gBAAgB,KAAK,IAAI,IAAI,OAAO;AACxE;AAEA,SAAS,gBAAgB,WAA2B;AAClD,SAAO,MAAM,KAAK,WAAW,CAAC,SAAS;AACrC,UAAM,QAAQ,KAAK,YAAY,CAAC,KAAK;AACrC,WAAO,QAAQ,QAAS,OAAO,MAAM,SAAS,EAAE,CAAC,MAAM,MAAM,MAAM,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC;AAAA,EAClG,CAAC,EAAE,KAAK,EAAE;AACZ;AAEO,SAAS,aAAa,OAAwB;AACnD,SAAO,qBAAqB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AACpF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// @generated by scripts/build-runtime.mjs; do not edit.
|
|
2
|
+
// @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
|
|
3
|
+
|
|
4
|
+
// src/scope/paths.ts
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
function normalizePath(input, toolPath) {
|
|
10
|
+
let normalized = toolPath ? input.replace(/[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g, " ") : input;
|
|
11
|
+
if (toolPath && normalized.startsWith("@")) normalized = normalized.slice(1);
|
|
12
|
+
if (process.platform === "win32" && normalized.startsWith("/") && !normalized.startsWith("//") && !normalized.includes("\\")) {
|
|
13
|
+
const match = normalized.match(/^\/(?:mnt\/|cygdrive\/)?([a-z])(?:\/(.*))?$/i);
|
|
14
|
+
if (match) normalized = `${match[1].toUpperCase()}:\\${match[2]?.replaceAll("/", "\\") ?? ""}`;
|
|
15
|
+
}
|
|
16
|
+
const home = homedir();
|
|
17
|
+
if (normalized === "~") return home;
|
|
18
|
+
if (normalized.startsWith("~/") || process.platform === "win32" && normalized.startsWith("~\\")) {
|
|
19
|
+
return join(home, normalized.slice(2));
|
|
20
|
+
}
|
|
21
|
+
return /^file:\/\//.test(normalized) ? fileURLToPath(normalized) : normalized;
|
|
22
|
+
}
|
|
23
|
+
function resolveWritePath(raw, cwd) {
|
|
24
|
+
const normalized = normalizePath(raw, true);
|
|
25
|
+
const base = normalizePath(cwd, false);
|
|
26
|
+
return isAbsolute(normalized) ? resolve(normalized) : resolve(base, normalized);
|
|
27
|
+
}
|
|
28
|
+
async function checked(signal, operation) {
|
|
29
|
+
signal?.throwIfAborted();
|
|
30
|
+
try {
|
|
31
|
+
return await operation();
|
|
32
|
+
} finally {
|
|
33
|
+
signal?.throwIfAborted();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function inspect(path, signal) {
|
|
37
|
+
try {
|
|
38
|
+
return await checked(signal, () => fs.lstat(path));
|
|
39
|
+
} catch (error) {
|
|
40
|
+
signal?.throwIfAborted();
|
|
41
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return void 0;
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function resolveScopeTarget(raw, cwd, requirement, signal) {
|
|
46
|
+
signal?.throwIfAborted();
|
|
47
|
+
return resolveLexicalTarget(resolveWritePath(raw, cwd), requirement, signal);
|
|
48
|
+
}
|
|
49
|
+
async function resolveLexicalTarget(path, requirement, signal) {
|
|
50
|
+
signal?.throwIfAborted();
|
|
51
|
+
let ancestor = path;
|
|
52
|
+
const suffix = [];
|
|
53
|
+
let entry = await inspect(ancestor, signal);
|
|
54
|
+
signal?.throwIfAborted();
|
|
55
|
+
while (!entry) {
|
|
56
|
+
const parent = dirname(ancestor);
|
|
57
|
+
if (parent === ancestor) throw new Error(`No existing ancestor for scope target ${JSON.stringify(path)}.`);
|
|
58
|
+
suffix.unshift(basename(ancestor));
|
|
59
|
+
ancestor = parent;
|
|
60
|
+
entry = await inspect(ancestor, signal);
|
|
61
|
+
signal?.throwIfAborted();
|
|
62
|
+
}
|
|
63
|
+
const realAncestor = await checked(signal, () => fs.realpath(ancestor));
|
|
64
|
+
const stats = await checked(signal, () => fs.stat(realAncestor));
|
|
65
|
+
const exists = suffix.length === 0;
|
|
66
|
+
if (exists) {
|
|
67
|
+
if (!stats.isFile()) throw new Error(`Scope target must be a regular file: ${JSON.stringify(path)}.`);
|
|
68
|
+
if (stats.nlink > 1) throw new Error(`Scope does not support hard-linked files: ${JSON.stringify(path)}.`);
|
|
69
|
+
} else if (!stats.isDirectory()) {
|
|
70
|
+
throw new Error(`Scope target parent must be a directory: ${JSON.stringify(ancestor)}.`);
|
|
71
|
+
}
|
|
72
|
+
if (requirement === "existing" && !exists) {
|
|
73
|
+
throw new Error(`Scope target does not exist: ${JSON.stringify(path)}.`);
|
|
74
|
+
}
|
|
75
|
+
if (requirement === "new" && exists) {
|
|
76
|
+
throw new Error(`Scope target already exists: ${JSON.stringify(path)}.`);
|
|
77
|
+
}
|
|
78
|
+
return { path, canonical: resolve(realAncestor, ...suffix), exists };
|
|
79
|
+
}
|
|
80
|
+
async function assertScopeTarget(raw, cwd, targets, signal) {
|
|
81
|
+
const candidate = await resolveScopeTarget(raw, cwd, "either", signal);
|
|
82
|
+
signal?.throwIfAborted();
|
|
83
|
+
const selected = targets.find((target) => target.canonical === candidate.canonical);
|
|
84
|
+
if (!selected) throw new Error("Scope blocks this target: it is not an allowed file.");
|
|
85
|
+
const refreshed = await resolveLexicalTarget(selected.path, "either", signal);
|
|
86
|
+
signal?.throwIfAborted();
|
|
87
|
+
if (refreshed.canonical !== selected.canonical) {
|
|
88
|
+
throw new Error("Scope target changed since selection. Select the allowed files again.");
|
|
89
|
+
}
|
|
90
|
+
const rechecked = await resolveScopeTarget(raw, cwd, "either", signal);
|
|
91
|
+
signal?.throwIfAborted();
|
|
92
|
+
if (rechecked.canonical !== selected.canonical) {
|
|
93
|
+
throw new Error("Scope mutation target changed during validation. Try again.");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export {
|
|
98
|
+
resolveScopeTarget,
|
|
99
|
+
assertScopeTarget
|
|
100
|
+
};
|
|
101
|
+
//# sourceMappingURL=chunk-F5QFC255.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/scope/paths.ts"],
|
|
4
|
+
"sourcesContent": ["import fs from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { basename, dirname, isAbsolute, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport interface ScopeTarget {\n /** Normalized absolute lexical path; do not apply tool input normalization again. */\n path: string;\n canonical: string;\n}\n\nexport interface ResolvedScopeTarget extends ScopeTarget {\n exists: boolean;\n}\n\n// Mirrors Pi's installed utils/paths.js and tools/path-utils.js write/edit path.\n// Do not trim, normalize Unicode composition, or use the read-only macOS fallbacks.\nfunction normalizePath(input: string, toolPath: boolean): string {\n let normalized = toolPath ? input.replace(/[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g, \" \") : input;\n if (toolPath && normalized.startsWith(\"@\")) normalized = normalized.slice(1);\n if (\n process.platform === \"win32\" &&\n normalized.startsWith(\"/\") &&\n !normalized.startsWith(\"//\") &&\n !normalized.includes(\"\\\\\")\n ) {\n const match = normalized.match(/^\\/(?:mnt\\/|cygdrive\\/)?([a-z])(?:\\/(.*))?$/i);\n if (match) normalized = `${match[1].toUpperCase()}:\\\\${match[2]?.replaceAll(\"/\", \"\\\\\") ?? \"\"}`;\n }\n const home = homedir();\n if (normalized === \"~\") return home;\n if (normalized.startsWith(\"~/\") || (process.platform === \"win32\" && normalized.startsWith(\"~\\\\\"))) {\n return join(home, normalized.slice(2));\n }\n return /^file:\\/\\//.test(normalized) ? fileURLToPath(normalized) : normalized;\n}\n\n/** The lexical target used by built-in write/edit; this does not access the filesystem. */\nexport function resolveWritePath(raw: string, cwd: string): string {\n const normalized = normalizePath(raw, true);\n const base = normalizePath(cwd, false);\n return isAbsolute(normalized) ? resolve(normalized) : resolve(base, normalized);\n}\n\n// Await OS work to completion before reporting cancellation; do not leave detached I/O.\nasync function checked<T>(signal: AbortSignal | undefined, operation: () => Promise<T>): Promise<T> {\n signal?.throwIfAborted();\n try {\n return await operation();\n } finally {\n signal?.throwIfAborted();\n }\n}\n\nasync function inspect(path: string, signal?: AbortSignal) {\n try {\n return await checked(signal, () => fs.lstat(path));\n } catch (error) {\n signal?.throwIfAborted();\n if (error && typeof error === \"object\" && \"code\" in error && error.code === \"ENOENT\") return undefined;\n throw error;\n }\n}\n\n/**\n * Resolve existing files, or a missing suffix below the nearest existing directory.\n * Symlinks may resolve to regular files/directories, but dangling links and hard-linked\n * files are rejected. Only ENOENT means missing; permission and other errors propagate.\n */\nexport async function resolveScopeTarget(\n raw: string,\n cwd: string,\n requirement: \"existing\" | \"new\" | \"either\",\n signal?: AbortSignal,\n): Promise<ResolvedScopeTarget> {\n signal?.throwIfAborted();\n return resolveLexicalTarget(resolveWritePath(raw, cwd), requirement, signal);\n}\n\nasync function resolveLexicalTarget(\n path: string,\n requirement: \"existing\" | \"new\" | \"either\",\n signal?: AbortSignal,\n): Promise<ResolvedScopeTarget> {\n signal?.throwIfAborted();\n let ancestor = path;\n const suffix: string[] = [];\n let entry = await inspect(ancestor, signal);\n signal?.throwIfAborted();\n while (!entry) {\n const parent = dirname(ancestor);\n if (parent === ancestor) throw new Error(`No existing ancestor for scope target ${JSON.stringify(path)}.`);\n suffix.unshift(basename(ancestor));\n ancestor = parent;\n entry = await inspect(ancestor, signal);\n signal?.throwIfAborted();\n }\n\n // realpath must succeed even for symlinks: an ENOENT here is a dangling link,\n // not a missing target which write is permitted to create.\n const realAncestor = await checked(signal, () => fs.realpath(ancestor));\n const stats = await checked(signal, () => fs.stat(realAncestor));\n const exists = suffix.length === 0;\n if (exists) {\n if (!stats.isFile()) throw new Error(`Scope target must be a regular file: ${JSON.stringify(path)}.`);\n if (stats.nlink > 1) throw new Error(`Scope does not support hard-linked files: ${JSON.stringify(path)}.`);\n } else if (!stats.isDirectory()) {\n throw new Error(`Scope target parent must be a directory: ${JSON.stringify(ancestor)}.`);\n }\n if (requirement === \"existing\" && !exists) {\n throw new Error(`Scope target does not exist: ${JSON.stringify(path)}.`);\n }\n if (requirement === \"new\" && exists) {\n throw new Error(`Scope target already exists: ${JSON.stringify(path)}.`);\n }\n return { path, canonical: resolve(realAncestor, ...suffix), exists };\n}\n\n/** Exact canonical matching, with fresh checks of both the reservation and mutation. */\nexport async function assertScopeTarget(\n raw: string,\n cwd: string,\n targets: readonly ScopeTarget[],\n signal?: AbortSignal,\n): Promise<void> {\n const candidate = await resolveScopeTarget(raw, cwd, \"either\", signal);\n signal?.throwIfAborted();\n const selected = targets.find((target) => target.canonical === candidate.canonical);\n if (!selected) throw new Error(\"Scope blocks this target: it is not an allowed file.\");\n const refreshed = await resolveLexicalTarget(selected.path, \"either\", signal);\n signal?.throwIfAborted();\n if (refreshed.canonical !== selected.canonical) {\n throw new Error(\"Scope target changed since selection. Select the allowed files again.\");\n }\n const rechecked = await resolveScopeTarget(raw, cwd, \"either\", signal);\n signal?.throwIfAborted();\n if (rechecked.canonical !== selected.canonical) {\n throw new Error(\"Scope mutation target changed during validation. Try again.\");\n }\n // This is a trusted-extension guard, not race-free OS containment. Pi performs\n // the mutation later; another process can still change filesystem entries.\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|