@orion-agents/orion-code 0.3.14 → 0.3.17
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 +286 -0
- package/README.md +6 -8
- package/README.zh-CN.md +4 -5
- package/dist/runtime/release-receipts.d.ts +15 -3
- package/dist/runtime/release-receipts.js +27 -1
- package/dist/services/session-storage.d.ts +9 -0
- package/dist/services/session-storage.js +27 -1
- package/dist/web/git-compare-service.d.ts +45 -0
- package/dist/web/git-compare-service.js +160 -0
- package/dist/web/git-diff-document.d.ts +83 -0
- package/dist/web/git-diff-document.js +214 -0
- package/dist/web/git-file-source.d.ts +25 -0
- package/dist/web/git-file-source.js +21 -0
- package/dist/web/git-history-service.d.ts +151 -0
- package/dist/web/git-history-service.js +610 -0
- package/dist/web/git-index-transaction.d.ts +45 -0
- package/dist/web/git-index-transaction.js +174 -0
- package/dist/web/git-patch-builder.d.ts +30 -0
- package/dist/web/git-patch-builder.js +134 -0
- package/dist/web/git-read-model-service.d.ts +338 -18
- package/dist/web/git-read-model-service.js +1029 -110
- package/dist/web/git-version-service.d.ts +71 -0
- package/dist/web/git-version-service.js +255 -0
- package/dist/web/native-directory-picker.d.ts +63 -0
- package/dist/web/native-directory-picker.js +169 -0
- package/dist/web/protocol.d.ts +51 -2
- package/dist/web/server.js +384 -8
- package/dist/web/workbench-controller.d.ts +126 -10
- package/dist/web/workbench-controller.js +383 -15
- package/dist/web/workspace-open-telemetry.d.ts +55 -0
- package/dist/web/workspace-open-telemetry.js +73 -0
- package/dist/web-client/assets/FilesPanel-BARQCmne.js +1 -0
- package/dist/web-client/assets/GitPanel-DjB9eSiJ.js +4 -0
- package/dist/web-client/assets/ReviewPanel-YhEGlp-G.js +5 -0
- package/dist/web-client/assets/{TerminalPanel-IkCttxgw.js → TerminalPanel-DnWh3Vp2.js} +1 -1
- package/dist/web-client/assets/index--hnGXXjj.css +1 -0
- package/dist/web-client/assets/index-Drja0_BT.js +20 -0
- package/dist/web-client/assets/useAutoNotice-DfQi4G1X.js +1 -0
- package/dist/web-client/favicon.svg +9 -0
- package/dist/web-client/index.html +3 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/web-client/assets/DiffViewer-BrHqudpK.js +0 -5
- package/dist/web-client/assets/FilesPanel-BcFY6L1a.js +0 -1
- package/dist/web-client/assets/GitPanel-B6E5IEXm.js +0 -1
- package/dist/web-client/assets/ResourceSplitLayout-fPYE9JKJ.js +0 -1
- package/dist/web-client/assets/ReviewPanel-5ecTVmhT.js +0 -1
- package/dist/web-client/assets/index-DgVNIqmE.js +0 -20
- package/dist/web-client/assets/index-R6E589Fc.css +0 -1
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.writeGitEnvironment = writeGitEnvironment;
|
|
4
|
+
exports.writeGitPrefix = writeGitPrefix;
|
|
5
|
+
exports.indexLockPath = indexLockPath;
|
|
6
|
+
exports.withIndexTransaction = withIndexTransaction;
|
|
7
|
+
exports.isIndexConflictOutput = isIndexConflictOutput;
|
|
8
|
+
/**
|
|
9
|
+
* v0.3.17 S3 — index transaction.
|
|
10
|
+
*
|
|
11
|
+
* Plan §7.7 is explicit: an internal queue cannot defend against external Git, and checking
|
|
12
|
+
* status before and after is **not** an atomic compare-and-swap. This module implements the
|
|
13
|
+
* real check-then-write window:
|
|
14
|
+
*
|
|
15
|
+
* 1. acquire `<index>.lock` with `O_CREAT|O_EXCL` — Git's own mutual exclusion, so a
|
|
16
|
+
* concurrent `git add` in the user's terminal fails instead of interleaving;
|
|
17
|
+
* 2. copy the current index to a sibling temp file and do **all** index work against
|
|
18
|
+
* `GIT_INDEX_FILE=<temp>`, so a failure never leaves a half-written index;
|
|
19
|
+
* 3. verify the result against the caller's expectations;
|
|
20
|
+
* 4. publish with a single `rename()` — atomic on POSIX, same filesystem by construction;
|
|
21
|
+
* 5. release only the lock this transaction created.
|
|
22
|
+
*
|
|
23
|
+
* What it deliberately does NOT do:
|
|
24
|
+
* - never removes a lock it did not create (a lock owned by the user's Git is reported as
|
|
25
|
+
* `git_index_busy` and left alone);
|
|
26
|
+
* - never rolls back external changes after a failure (§7.9).
|
|
27
|
+
*/
|
|
28
|
+
const fs_1 = require("fs");
|
|
29
|
+
const crypto_1 = require("crypto");
|
|
30
|
+
const path_1 = require("path");
|
|
31
|
+
const errors_1 = require("./errors");
|
|
32
|
+
const GIT_NULL_DEVICE = process.platform === 'win32' ? 'NUL' : '/dev/null';
|
|
33
|
+
/** Non-secret marker proving we own a lock file, so we never unlink someone else's. */
|
|
34
|
+
const LOCK_MARKER_PREFIX = 'orion-code index transaction ';
|
|
35
|
+
/**
|
|
36
|
+
* Environment for index writes.
|
|
37
|
+
*
|
|
38
|
+
* `GIT_CONFIG_GLOBAL` is intentionally *not* pointed at /dev/null here, unlike the read
|
|
39
|
+
* path: staging must resolve the user's `core.autocrlf`, filters and `.gitattributes`
|
|
40
|
+
* exactly as their own Git would, otherwise we would silently stage different bytes than
|
|
41
|
+
* they expect (plan §7.8). Hooks stay disabled — `add`/`apply` must not run user hooks.
|
|
42
|
+
*/
|
|
43
|
+
function writeGitEnvironment(overrides = {}) {
|
|
44
|
+
return {
|
|
45
|
+
PATH: process.env.PATH,
|
|
46
|
+
HOME: process.env.HOME,
|
|
47
|
+
LANG: 'C.UTF-8',
|
|
48
|
+
LC_ALL: 'C.UTF-8',
|
|
49
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
50
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
51
|
+
GIT_PAGER: 'cat',
|
|
52
|
+
PAGER: 'cat',
|
|
53
|
+
...overrides,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* `-c` prefix for index writes. Hooks are disabled because none of the index mutations run
|
|
58
|
+
* hooks; the commit runner uses its own prefix and does not reuse this one.
|
|
59
|
+
*/
|
|
60
|
+
function writeGitPrefix() {
|
|
61
|
+
return [
|
|
62
|
+
'-c',
|
|
63
|
+
'core.quotepath=false',
|
|
64
|
+
'-c',
|
|
65
|
+
`core.hooksPath=${GIT_NULL_DEVICE}`,
|
|
66
|
+
'--literal-pathspecs',
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
function indexLockPath(indexPath) {
|
|
70
|
+
return `${indexPath}.lock`;
|
|
71
|
+
}
|
|
72
|
+
function acquireIndexLock(indexPath) {
|
|
73
|
+
const path = indexLockPath(indexPath);
|
|
74
|
+
const marker = `${LOCK_MARKER_PREFIX}${process.pid} ${(0, crypto_1.randomBytes)(8).toString('hex')}\n`;
|
|
75
|
+
let handle;
|
|
76
|
+
try {
|
|
77
|
+
handle = (0, fs_1.openSync)(path, 'wx', 0o600);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error.code === 'EEXIST') {
|
|
81
|
+
// Someone else holds it. Report busy and leave their lock untouched.
|
|
82
|
+
throw new errors_1.WebWorkbenchError(409, 'Git index is locked by another process.', 'git_index_busy');
|
|
83
|
+
}
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
(0, fs_1.writeSync)(handle, marker);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
(0, fs_1.closeSync)(handle);
|
|
91
|
+
}
|
|
92
|
+
return { path, marker };
|
|
93
|
+
}
|
|
94
|
+
function releaseIndexLock(lock) {
|
|
95
|
+
try {
|
|
96
|
+
// Only remove a lock whose content is still ours. If the user's Git replaced it, that is
|
|
97
|
+
// their lock now and we must not delete it.
|
|
98
|
+
const current = (0, fs_1.readFileSync)(lock.path, 'utf8');
|
|
99
|
+
if (!current.startsWith(LOCK_MARKER_PREFIX) || current !== lock.marker)
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
(0, fs_1.unlinkSync)(lock.path);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Already gone; nothing to release.
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Runs `work` against a private copy of the index and publishes it atomically.
|
|
114
|
+
*
|
|
115
|
+
* `verify` receives the temp index path so the caller can assert the mutation actually did
|
|
116
|
+
* what it intended (for example that the target path is now staged) before publication.
|
|
117
|
+
*/
|
|
118
|
+
async function withIndexTransaction(paths, seed, work, verify) {
|
|
119
|
+
const lock = acquireIndexLock(paths.indexPath);
|
|
120
|
+
const tempIndex = (0, path_1.join)((0, path_1.dirname)(paths.indexPath), `orion-index-${process.pid}-${(0, crypto_1.randomBytes)(6).toString('hex')}.tmp`);
|
|
121
|
+
const env = writeGitEnvironment({ GIT_INDEX_FILE: tempIndex });
|
|
122
|
+
const before = (0, fs_1.existsSync)(paths.indexPath)
|
|
123
|
+
? (0, fs_1.readFileSync)(paths.indexPath).toString('base64').slice(0, 32)
|
|
124
|
+
: 'absent';
|
|
125
|
+
const context = { indexFile: tempIndex, env, before };
|
|
126
|
+
try {
|
|
127
|
+
// Seed the private index from the real one, or from an empty tree in a repository that
|
|
128
|
+
// has no index yet (fresh clone-less init, or a worktree created before any add).
|
|
129
|
+
if ((0, fs_1.existsSync)(paths.indexPath)) {
|
|
130
|
+
(0, fs_1.copyFileSync)(paths.indexPath, tempIndex);
|
|
131
|
+
// Copy the ORIGINAL timestamps onto the copy.
|
|
132
|
+
//
|
|
133
|
+
// This is not cosmetic. Git decides whether a cached stat entry may be trusted by
|
|
134
|
+
// asking whether the index is at least as new as the file. A freshly copied index is
|
|
135
|
+
// newer than everything, which makes git trust stale stat data — so a file edited
|
|
136
|
+
// within the same timestamp granularity and with an unchanged size would not be
|
|
137
|
+
// re-hashed, and `git add` would silently do nothing. Preserving the mtime keeps the
|
|
138
|
+
// racy-clean defence that a real index has.
|
|
139
|
+
const stats = (0, fs_1.statSync)(paths.indexPath);
|
|
140
|
+
(0, fs_1.utimesSync)(tempIndex, stats.atime, stats.mtime);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
await seed(context);
|
|
144
|
+
}
|
|
145
|
+
const result = await work(context);
|
|
146
|
+
if (verify)
|
|
147
|
+
await verify(result, context);
|
|
148
|
+
// Single atomic publish; the real index is never partially written.
|
|
149
|
+
(0, fs_1.renameSync)(tempIndex, paths.indexPath);
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if ((0, fs_1.existsSync)(tempIndex)) {
|
|
154
|
+
try {
|
|
155
|
+
(0, fs_1.unlinkSync)(tempIndex);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Best effort; a leftover temp index is inert because nothing reads it.
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
releaseIndexLock(lock);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Failure text Git prints when it refuses because the index changed under it. Surfaced so
|
|
169
|
+
* callers can report an honest conflict instead of a generic error.
|
|
170
|
+
*/
|
|
171
|
+
function isIndexConflictOutput(text) {
|
|
172
|
+
return /index file smaller than expected|index file corrupt|unable to read index|needs update/i.test(text);
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=git-index-transaction.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.3.17 S3 — Host-side patch builder for hunk and line level staging.
|
|
3
|
+
*
|
|
4
|
+
* Plan §7.6 is unambiguous: the Host rebuilds the patch from the content the user actually
|
|
5
|
+
* reviewed, and the browser never submits a patch, a path, a Git flag or a shell command.
|
|
6
|
+
* §7.8 adds the other half: a partial operation is **refused** when the target bytes cannot
|
|
7
|
+
* be reconstructed faithfully, rather than guessing and staging different content.
|
|
8
|
+
*
|
|
9
|
+
* Reconstruction rules for a partial selection of one hunk:
|
|
10
|
+
* - an unselected deletion becomes context (the line stays in the index);
|
|
11
|
+
* - an unselected addition is dropped entirely (it never enters the index);
|
|
12
|
+
* - selected deletions/additions keep their marker.
|
|
13
|
+
* That is exactly "apply only what was ticked", expressed as a patch whose pre-image is the
|
|
14
|
+
* untouched index and whose post-image is index + selection.
|
|
15
|
+
*/
|
|
16
|
+
import type { GitDiffDocumentV2 } from './git-diff-document';
|
|
17
|
+
export interface PatchSelectionRequest {
|
|
18
|
+
readonly hunkIds?: readonly string[];
|
|
19
|
+
readonly lineIds?: readonly string[];
|
|
20
|
+
}
|
|
21
|
+
export type PatchBuildResult = {
|
|
22
|
+
readonly ok: true;
|
|
23
|
+
readonly patch: string;
|
|
24
|
+
readonly selectedChanges: number;
|
|
25
|
+
} | {
|
|
26
|
+
readonly ok: false;
|
|
27
|
+
readonly reason: string;
|
|
28
|
+
};
|
|
29
|
+
export declare function buildSelectionPatch(doc: GitDiffDocumentV2, request: PatchSelectionRequest): PatchBuildResult;
|
|
30
|
+
//# sourceMappingURL=git-patch-builder.d.ts.map
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildSelectionPatch = buildSelectionPatch;
|
|
4
|
+
/** Sources whose comparison can be expressed as an index patch. */
|
|
5
|
+
const PATCHABLE_SOURCES = new Set(['unstaged', 'staged']);
|
|
6
|
+
function refusal(doc) {
|
|
7
|
+
if (!PATCHABLE_SOURCES.has(doc.source)) {
|
|
8
|
+
return doc.source === 'untracked'
|
|
9
|
+
? '未跟踪文件首版仅支持整文件暂存。'
|
|
10
|
+
: '冲突文件本版不提供部分暂存,请在 Files 中处理。';
|
|
11
|
+
}
|
|
12
|
+
if (doc.kind !== 'text') {
|
|
13
|
+
return doc.kind === 'binary'
|
|
14
|
+
? '二进制差异无法重建为可应用的 patch。'
|
|
15
|
+
: '仅元数据(mode/rename)变化无法进行部分暂存。';
|
|
16
|
+
}
|
|
17
|
+
if (doc.completeness !== 'complete') {
|
|
18
|
+
return 'Diff 未完整加载,未加载的 Hunk 不能执行任何写入。';
|
|
19
|
+
}
|
|
20
|
+
if (doc.hunks.some(hunk => !hunk.complete)) {
|
|
21
|
+
return '存在未完整加载的 Hunk,无法重建可应用的 patch。';
|
|
22
|
+
}
|
|
23
|
+
if (doc.hunks.length === 0) {
|
|
24
|
+
return '没有可暂存的文本改动。';
|
|
25
|
+
}
|
|
26
|
+
// A partial patch for an added/removed/renamed file needs a different pre-image shape.
|
|
27
|
+
// Rather than emit a patch we cannot prove, the file stays a whole-file operation
|
|
28
|
+
// (plan §7.8: refuse when the target bytes cannot be reconstructed faithfully).
|
|
29
|
+
const structural = doc.meta.find(line => /^(new file mode|deleted file mode|rename from|rename to|old mode|new mode|similarity index)/u.test(line));
|
|
30
|
+
if (structural) {
|
|
31
|
+
return '新增、删除、重命名或纯权限变化的文件不支持部分暂存,请使用整文件操作。';
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
/** Escapes a path for the `diff --git` / `---` / `+++` headers. */
|
|
36
|
+
function patchPath(path) {
|
|
37
|
+
return /[\s"\\]/u.test(path) ? `"${path.replace(/\\/gu, '\\\\').replace(/"/gu, '\\"')}"` : path;
|
|
38
|
+
}
|
|
39
|
+
function buildHunkBody(hunk, selection) {
|
|
40
|
+
const body = [];
|
|
41
|
+
let oldCount = 0;
|
|
42
|
+
let newCount = 0;
|
|
43
|
+
let changes = 0;
|
|
44
|
+
for (const line of hunk.lines) {
|
|
45
|
+
const selected = selection.whole || selection.lineIds.has(line.lineId);
|
|
46
|
+
if (line.kind === 'context') {
|
|
47
|
+
body.push(line.raw);
|
|
48
|
+
oldCount += 1;
|
|
49
|
+
newCount += 1;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (line.kind === 'meta') {
|
|
53
|
+
// `` only belongs after a change we are actually applying.
|
|
54
|
+
if (selected)
|
|
55
|
+
body.push(line.raw);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (line.kind === 'deletion') {
|
|
59
|
+
if (selected) {
|
|
60
|
+
body.push(line.raw);
|
|
61
|
+
oldCount += 1;
|
|
62
|
+
changes += 1;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// Kept in the index: becomes context.
|
|
66
|
+
body.push(` ${line.text}`);
|
|
67
|
+
oldCount += 1;
|
|
68
|
+
newCount += 1;
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
// addition
|
|
73
|
+
if (selected) {
|
|
74
|
+
body.push(line.raw);
|
|
75
|
+
newCount += 1;
|
|
76
|
+
changes += 1;
|
|
77
|
+
}
|
|
78
|
+
// An unselected addition is simply absent from the post-image.
|
|
79
|
+
}
|
|
80
|
+
return { body, oldCount, newCount, changes };
|
|
81
|
+
}
|
|
82
|
+
function buildSelectionPatch(doc, request) {
|
|
83
|
+
const blocked = refusal(doc);
|
|
84
|
+
if (blocked)
|
|
85
|
+
return { ok: false, reason: blocked };
|
|
86
|
+
const wholeHunks = new Set(request.hunkIds ?? []);
|
|
87
|
+
const selectedLines = new Set(request.lineIds ?? []);
|
|
88
|
+
if (wholeHunks.size === 0 && selectedLines.size === 0) {
|
|
89
|
+
return { ok: false, reason: '没有选择任何 Hunk 或行。' };
|
|
90
|
+
}
|
|
91
|
+
// Selected line ids must belong to this document; an unknown id means the reader's view is
|
|
92
|
+
// stale and applying anything would be guesswork.
|
|
93
|
+
if (selectedLines.size > 0) {
|
|
94
|
+
const known = new Set(doc.hunks.flatMap(hunk => hunk.lines.map(line => line.lineId)));
|
|
95
|
+
for (const lineId of selectedLines) {
|
|
96
|
+
if (!known.has(lineId)) {
|
|
97
|
+
return { ok: false, reason: '选中的行已不在当前 Diff 中,请刷新后重试。' };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Standard orientation for both directions. Unstaging is the same patch applied with
|
|
102
|
+
// `git apply --reverse`, so the headers must NOT be swapped for the staged source.
|
|
103
|
+
const headers = [
|
|
104
|
+
`diff --git a/${patchPath(doc.path)} b/${patchPath(doc.path)}`,
|
|
105
|
+
`--- a/${patchPath(doc.path)}`,
|
|
106
|
+
`+++ b/${patchPath(doc.path)}`,
|
|
107
|
+
];
|
|
108
|
+
const bodyParts = [];
|
|
109
|
+
let cumulativeDelta = 0;
|
|
110
|
+
let totalChanges = 0;
|
|
111
|
+
for (const hunk of doc.hunks) {
|
|
112
|
+
const whole = wholeHunks.has(hunk.hunkId);
|
|
113
|
+
const built = buildHunkBody(hunk, { whole, lineIds: selectedLines });
|
|
114
|
+
// A hunk with nothing ticked contributes neither content nor offset.
|
|
115
|
+
if (built.changes === 0)
|
|
116
|
+
continue;
|
|
117
|
+
const appliedDelta = built.newCount - built.oldCount;
|
|
118
|
+
const oldStart = hunk.oldStart;
|
|
119
|
+
// The post-image offset absorbs only the deltas we are actually applying.
|
|
120
|
+
const newStart = oldStart + cumulativeDelta;
|
|
121
|
+
cumulativeDelta += appliedDelta;
|
|
122
|
+
totalChanges += built.changes;
|
|
123
|
+
bodyParts.push(`@@ -${oldStart},${built.oldCount} +${newStart},${built.newCount} @@${hunk.contextLabel ? ` ${hunk.contextLabel}` : ''}`, ...built.body);
|
|
124
|
+
}
|
|
125
|
+
if (totalChanges === 0) {
|
|
126
|
+
return { ok: false, reason: '选中的内容没有实际改动。' };
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
ok: true,
|
|
130
|
+
patch: `${[...headers, ...bodyParts].join('\n')}\n`,
|
|
131
|
+
selectedChanges: totalChanges,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=git-patch-builder.js.map
|