@orion-agents/orion-code 0.3.0 → 0.3.2

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.
Files changed (172) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +45 -18
  3. package/README.zh-CN.md +39 -18
  4. package/dist/commands/model-command-handlers.d.ts.map +1 -1
  5. package/dist/commands/model-command-handlers.js +59 -14
  6. package/dist/commands/model-command-handlers.js.map +1 -1
  7. package/dist/commands/types.d.ts +6 -0
  8. package/dist/commands/types.d.ts.map +1 -1
  9. package/dist/commands/types.js.map +1 -1
  10. package/dist/commands/workflow-command-handlers.d.ts +1 -1
  11. package/dist/commands/workflow-command-handlers.d.ts.map +1 -1
  12. package/dist/commands/workflow-command-handlers.js +42 -3
  13. package/dist/commands/workflow-command-handlers.js.map +1 -1
  14. package/dist/commands/workflow-commands.d.ts.map +1 -1
  15. package/dist/commands/workflow-commands.js +2 -2
  16. package/dist/commands/workflow-commands.js.map +1 -1
  17. package/dist/core/tool-artifacts.d.ts +2 -0
  18. package/dist/core/tool-artifacts.d.ts.map +1 -1
  19. package/dist/core/tool-artifacts.js +27 -1
  20. package/dist/core/tool-artifacts.js.map +1 -1
  21. package/dist/runtime/agent-runtime-controller.d.ts +16 -2
  22. package/dist/runtime/agent-runtime-controller.d.ts.map +1 -1
  23. package/dist/runtime/agent-runtime-controller.js +116 -8
  24. package/dist/runtime/agent-runtime-controller.js.map +1 -1
  25. package/dist/runtime/agent-runtime-protocol.d.ts +7 -0
  26. package/dist/runtime/agent-runtime-protocol.d.ts.map +1 -1
  27. package/dist/runtime/agent-runtime-protocol.js.map +1 -1
  28. package/dist/runtime/agent-runtime-runner.d.ts +8 -0
  29. package/dist/runtime/agent-runtime-runner.d.ts.map +1 -1
  30. package/dist/runtime/durable-tool-receipt-reader.d.ts +32 -0
  31. package/dist/runtime/durable-tool-receipt-reader.d.ts.map +1 -0
  32. package/dist/runtime/durable-tool-receipt-reader.js +130 -0
  33. package/dist/runtime/durable-tool-receipt-reader.js.map +1 -0
  34. package/dist/runtime/legacy-thread-materializer.d.ts +14 -1
  35. package/dist/runtime/legacy-thread-materializer.d.ts.map +1 -1
  36. package/dist/runtime/legacy-thread-materializer.js +44 -5
  37. package/dist/runtime/legacy-thread-materializer.js.map +1 -1
  38. package/dist/runtime/orion-session-runner.d.ts +4 -0
  39. package/dist/runtime/orion-session-runner.d.ts.map +1 -1
  40. package/dist/runtime/orion-session-runner.js +15 -0
  41. package/dist/runtime/orion-session-runner.js.map +1 -1
  42. package/dist/runtime/plan-review.d.ts +38 -0
  43. package/dist/runtime/plan-review.d.ts.map +1 -0
  44. package/dist/runtime/plan-review.js +173 -0
  45. package/dist/runtime/plan-review.js.map +1 -0
  46. package/dist/runtime/product-bootstrap.d.ts.map +1 -1
  47. package/dist/runtime/product-bootstrap.js +28 -6
  48. package/dist/runtime/product-bootstrap.js.map +1 -1
  49. package/dist/runtime/product-orion-runtime.d.ts.map +1 -1
  50. package/dist/runtime/product-orion-runtime.js +14 -35
  51. package/dist/runtime/product-orion-runtime.js.map +1 -1
  52. package/dist/runtime/protocol/runtime-protocol-v1.d.ts +73 -0
  53. package/dist/runtime/protocol/runtime-protocol-v1.d.ts.map +1 -1
  54. package/dist/runtime/protocol/runtime-protocol-v1.js +37 -0
  55. package/dist/runtime/protocol/runtime-protocol-v1.js.map +1 -1
  56. package/dist/runtime/release-receipts.d.ts +8 -3
  57. package/dist/runtime/release-receipts.d.ts.map +1 -1
  58. package/dist/runtime/release-receipts.js +88 -4
  59. package/dist/runtime/release-receipts.js.map +1 -1
  60. package/dist/runtime/session-composer-control.d.ts +136 -0
  61. package/dist/runtime/session-composer-control.d.ts.map +1 -0
  62. package/dist/runtime/session-composer-control.js +532 -0
  63. package/dist/runtime/session-composer-control.js.map +1 -0
  64. package/dist/runtime/thread-event-store.d.ts +62 -9
  65. package/dist/runtime/thread-event-store.d.ts.map +1 -1
  66. package/dist/runtime/thread-event-store.js +363 -31
  67. package/dist/runtime/thread-event-store.js.map +1 -1
  68. package/dist/runtime/thread-projection.d.ts +17 -0
  69. package/dist/runtime/thread-projection.d.ts.map +1 -1
  70. package/dist/runtime/thread-projection.js +87 -0
  71. package/dist/runtime/thread-projection.js.map +1 -1
  72. package/dist/runtime/thread-session-index.d.ts +84 -0
  73. package/dist/runtime/thread-session-index.d.ts.map +1 -0
  74. package/dist/runtime/thread-session-index.js +503 -0
  75. package/dist/runtime/thread-session-index.js.map +1 -0
  76. package/dist/runtime/thread-session-view.d.ts +31 -13
  77. package/dist/runtime/thread-session-view.d.ts.map +1 -1
  78. package/dist/runtime/thread-session-view.js +116 -61
  79. package/dist/runtime/thread-session-view.js.map +1 -1
  80. package/dist/runtime/thread-ui-adapter.d.ts.map +1 -1
  81. package/dist/runtime/thread-ui-adapter.js +18 -58
  82. package/dist/runtime/thread-ui-adapter.js.map +1 -1
  83. package/dist/runtime/tool-detail-repository.d.ts.map +1 -1
  84. package/dist/runtime/tool-detail-repository.js +23 -19
  85. package/dist/runtime/tool-detail-repository.js.map +1 -1
  86. package/dist/runtime/tool-receipt-validator.d.ts +21 -0
  87. package/dist/runtime/tool-receipt-validator.d.ts.map +1 -0
  88. package/dist/runtime/tool-receipt-validator.js +157 -0
  89. package/dist/runtime/tool-receipt-validator.js.map +1 -0
  90. package/dist/runtime/ui-events.d.ts +6 -1
  91. package/dist/runtime/ui-events.d.ts.map +1 -1
  92. package/dist/runtime/ui-events.js.map +1 -1
  93. package/dist/services/redaction.d.ts +4 -0
  94. package/dist/services/redaction.d.ts.map +1 -1
  95. package/dist/services/redaction.js +63 -0
  96. package/dist/services/redaction.js.map +1 -1
  97. package/dist/services/session-index.d.ts +16 -0
  98. package/dist/services/session-index.d.ts.map +1 -1
  99. package/dist/services/session-index.js +45 -36
  100. package/dist/services/session-index.js.map +1 -1
  101. package/dist/services/session-storage.d.ts +36 -0
  102. package/dist/services/session-storage.d.ts.map +1 -1
  103. package/dist/services/session-storage.js +80 -7
  104. package/dist/services/session-storage.js.map +1 -1
  105. package/dist/services/workspace-registry.d.ts +42 -0
  106. package/dist/services/workspace-registry.d.ts.map +1 -0
  107. package/dist/services/workspace-registry.js +222 -0
  108. package/dist/services/workspace-registry.js.map +1 -0
  109. package/dist/web/errors.d.ts +6 -0
  110. package/dist/web/errors.d.ts.map +1 -0
  111. package/dist/web/errors.js +13 -0
  112. package/dist/web/errors.js.map +1 -0
  113. package/dist/web/event-hub.d.ts +4 -0
  114. package/dist/web/event-hub.d.ts.map +1 -1
  115. package/dist/web/event-hub.js +81 -11
  116. package/dist/web/event-hub.js.map +1 -1
  117. package/dist/web/file-read-service.d.ts +64 -0
  118. package/dist/web/file-read-service.d.ts.map +1 -0
  119. package/dist/web/file-read-service.js +409 -0
  120. package/dist/web/file-read-service.js.map +1 -0
  121. package/dist/web/git-read-model-service.d.ts +85 -0
  122. package/dist/web/git-read-model-service.d.ts.map +1 -0
  123. package/dist/web/git-read-model-service.js +657 -0
  124. package/dist/web/git-read-model-service.js.map +1 -0
  125. package/dist/web/protocol.d.ts +205 -1
  126. package/dist/web/protocol.d.ts.map +1 -1
  127. package/dist/web/protocol.js +229 -1
  128. package/dist/web/protocol.js.map +1 -1
  129. package/dist/web/review-service.d.ts +38 -0
  130. package/dist/web/review-service.d.ts.map +1 -0
  131. package/dist/web/review-service.js +74 -0
  132. package/dist/web/review-service.js.map +1 -0
  133. package/dist/web/server.d.ts.map +1 -1
  134. package/dist/web/server.js +312 -29
  135. package/dist/web/server.js.map +1 -1
  136. package/dist/web/terminal-manager.d.ts +156 -0
  137. package/dist/web/terminal-manager.d.ts.map +1 -0
  138. package/dist/web/terminal-manager.js +760 -0
  139. package/dist/web/terminal-manager.js.map +1 -0
  140. package/dist/web/terminal-server.d.ts +8 -0
  141. package/dist/web/terminal-server.d.ts.map +1 -0
  142. package/dist/web/terminal-server.js +275 -0
  143. package/dist/web/terminal-server.js.map +1 -0
  144. package/dist/web/workbench-controller.d.ts +71 -18
  145. package/dist/web/workbench-controller.d.ts.map +1 -1
  146. package/dist/web/workbench-controller.js +1187 -178
  147. package/dist/web/workbench-controller.js.map +1 -1
  148. package/dist/web-client/assets/DiffViewer-eaQNsnCh.js +5 -0
  149. package/dist/web-client/assets/FilesPanel-DYs1MhoM.js +2 -0
  150. package/dist/web-client/assets/GitPanel-CnU4XVr7.js +1 -0
  151. package/dist/web-client/assets/ReviewPanel-BDxG670T.js +1 -0
  152. package/dist/web-client/assets/TerminalPanel-DLuoa74B.css +1 -0
  153. package/dist/web-client/assets/TerminalPanel-Q5mSSsoK.js +23 -0
  154. package/dist/web-client/assets/index-BWV5sIvw.css +1 -0
  155. package/dist/web-client/assets/index-VVUKj6FE.js +17 -0
  156. package/dist/web-client/index.html +2 -2
  157. package/docs/architecture/agent-mode-permission-contract.md +87 -0
  158. package/docs/architecture/v0.3.0-web-api.yaml +44 -13
  159. package/docs/architecture/v0.3.1-web-api.yaml +2338 -0
  160. package/docs/architecture/v0.3.2-web-api.yaml +3029 -0
  161. package/docs/migration/v0.3.0-to-v0.3.1.md +88 -0
  162. package/docs/migration/v0.3.1-to-v0.3.2.md +94 -0
  163. package/docs/plan/v0.3.0-web-workbench-plan.md +58 -7
  164. package/docs/plan/v0.3.1-web-workbench-professional-shell-plan.md +848 -0
  165. package/docs/plan/v0.3.2-web-workbench-layout-and-composer-plan.md +869 -0
  166. package/docs/readme.md +20 -0
  167. package/docs/test/v0.3.1-web-workbench-e2e-plan.md +140 -0
  168. package/docs/test/v0.3.2-web-workbench-e2e-plan.md +100 -0
  169. package/npm-shrinkwrap.json +185 -354
  170. package/package.json +21 -4
  171. package/dist/web-client/assets/index-BAEO71Gp.css +0 -1
  172. package/dist/web-client/assets/index-DSkFy7gM.js +0 -13
@@ -0,0 +1,657 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GitReadModelServiceV1 = void 0;
4
+ const child_process_1 = require("child_process");
5
+ const crypto_1 = require("crypto");
6
+ const fs_1 = require("fs");
7
+ const path_1 = require("path");
8
+ const redaction_1 = require("../services/redaction");
9
+ const errors_1 = require("./errors");
10
+ const GIT_TIMEOUT_MS = 5000;
11
+ const GIT_MAX_BUFFER = 8 * 1024 * 1024;
12
+ const DEFAULT_STATUS_PAGE_SIZE = 200;
13
+ const MAX_STATUS_PAGE_SIZE = 2000;
14
+ const DEFAULT_LOG_PAGE_SIZE = 30;
15
+ const MAX_LOG_PAGE_SIZE = 100;
16
+ const DEFAULT_DIFF_LINES = 240;
17
+ const MAX_DIFF_LINES = 500;
18
+ const DEFAULT_DIFF_BYTES = 256 * 1024;
19
+ const MAX_DIFF_BYTES = 1024 * 1024;
20
+ const GIT_NULL_DEVICE = process.platform === 'win32' ? 'NUL' : '/dev/null';
21
+ /** Bounded argv-only Git status/log/diff projection for one active Workspace. */
22
+ class GitReadModelServiceV1 {
23
+ constructor(workspace) {
24
+ this.secret = (0, crypto_1.randomBytes)(32);
25
+ this.pathById = new Map();
26
+ this.idByPath = new Map();
27
+ this.performance = { processCount: 0, bytesRead: 0, itemsParsed: 0 };
28
+ this.workspace = canonicalDirectory(workspace);
29
+ }
30
+ performanceCounters() {
31
+ return Object.freeze({ ...this.performance });
32
+ }
33
+ async status(input = {}) {
34
+ const snapshot = await this.capture();
35
+ const pageSize = boundedInteger(input.pageSize ?? DEFAULT_STATUS_PAGE_SIZE, 1, MAX_STATUS_PAGE_SIZE, 'pageSize');
36
+ const offset = input.cursor
37
+ ? this.decodeCursor(input.cursor, 'status', snapshot.revision).offset
38
+ : 0;
39
+ const records = snapshot.records.slice(offset, offset + pageSize);
40
+ const projected = records.map(record => this.projectFile(record));
41
+ const staged = projected.filter(file => file.indexStatus !== ' ' && file.indexStatus !== '?' && !isConflict(file));
42
+ const unstaged = projected.filter(file => file.worktreeStatus !== ' ' && file.worktreeStatus !== '?' && !isConflict(file));
43
+ const untracked = projected.filter(file => file.indexStatus === '?');
44
+ const conflicted = projected.filter(isConflict);
45
+ const nextOffset = offset + records.length;
46
+ return Object.freeze({
47
+ isRepository: snapshot.isRepository,
48
+ repositoryRevision: snapshot.revision,
49
+ ...(snapshot.root ? { rootLabel: (0, path_1.basename)(snapshot.root) } : {}),
50
+ branch: snapshot.branch,
51
+ detached: snapshot.isRepository && !snapshot.branch,
52
+ head: snapshot.head ? snapshot.head.slice(0, 12) : null,
53
+ upstream: snapshot.upstream,
54
+ ahead: snapshot.ahead,
55
+ behind: snapshot.behind,
56
+ clean: snapshot.records.length === 0,
57
+ staged: Object.freeze(staged),
58
+ unstaged: Object.freeze(unstaged),
59
+ untracked: Object.freeze(untracked),
60
+ conflicted: Object.freeze(conflicted),
61
+ totalFiles: snapshot.records.length,
62
+ truncated: nextOffset < snapshot.records.length,
63
+ nextCursor: nextOffset < snapshot.records.length
64
+ ? this.encodeCursor({
65
+ version: 1,
66
+ kind: 'status',
67
+ revision: snapshot.revision,
68
+ offset: nextOffset,
69
+ })
70
+ : null,
71
+ });
72
+ }
73
+ async log(input = {}) {
74
+ const snapshot = await this.capture();
75
+ if (!snapshot.isRepository || !snapshot.root || !snapshot.head) {
76
+ return Object.freeze({
77
+ repositoryRevision: snapshot.revision,
78
+ items: Object.freeze([]),
79
+ nextCursor: null,
80
+ });
81
+ }
82
+ const pageSize = boundedInteger(input.pageSize ?? DEFAULT_LOG_PAGE_SIZE, 1, MAX_LOG_PAGE_SIZE, 'pageSize');
83
+ const offset = input.cursor
84
+ ? this.decodeCursor(input.cursor, 'log', snapshot.revision).offset
85
+ : 0;
86
+ const raw = await this.runGit([
87
+ 'log',
88
+ `--skip=${offset}`,
89
+ `--max-count=${pageSize + 1}`,
90
+ '--format=%H%x1f%h%x1f%ct%x1f%an%x1f%s%x1e',
91
+ ], snapshot.root);
92
+ const commits = raw
93
+ .split('\x1e')
94
+ .map(record => record.replace(/^\s+/u, '').replace(/\s+$/u, ''))
95
+ .filter(Boolean)
96
+ .map(parseCommit);
97
+ this.performance.itemsParsed += commits.length;
98
+ const hasMore = commits.length > pageSize;
99
+ const items = commits.slice(0, pageSize);
100
+ return Object.freeze({
101
+ repositoryRevision: snapshot.revision,
102
+ items: Object.freeze(items),
103
+ nextCursor: hasMore
104
+ ? this.encodeCursor({
105
+ version: 1,
106
+ kind: 'log',
107
+ revision: snapshot.revision,
108
+ offset: offset + items.length,
109
+ })
110
+ : null,
111
+ });
112
+ }
113
+ async diff(input) {
114
+ const snapshot = await this.capture();
115
+ if (!snapshot.isRepository || !snapshot.root) {
116
+ throw new errors_1.WebWorkbenchError(404, 'Git repository is unavailable.', 'git_not_repository');
117
+ }
118
+ const path = this.pathById.get(input.fileId);
119
+ if (!path)
120
+ throw new errors_1.WebWorkbenchError(404, 'Git file was not found.', 'git_file_not_found');
121
+ const record = snapshot.records.find(candidate => candidate.path === path);
122
+ if (!record) {
123
+ throw new errors_1.WebWorkbenchError(409, 'Git file changed before diff.', 'git_revision_conflict');
124
+ }
125
+ const lineLimit = boundedInteger(input.lineLimit ?? DEFAULT_DIFF_LINES, 1, MAX_DIFF_LINES, 'lineLimit');
126
+ const byteLimit = boundedInteger(input.byteLimit ?? DEFAULT_DIFF_BYTES, 1024, MAX_DIFF_BYTES, 'byteLimit');
127
+ const offset = input.cursor
128
+ ? this.decodeCursor(input.cursor, 'diff', snapshot.revision, input.fileId).offset
129
+ : 0;
130
+ const commands = diffCommands(record, path);
131
+ const page = await streamDiffPage({
132
+ cwd: snapshot.root,
133
+ commands,
134
+ offset,
135
+ lineLimit,
136
+ byteLimit,
137
+ onProcess: () => {
138
+ this.performance.processCount += 1;
139
+ },
140
+ onBytes: bytes => {
141
+ this.performance.bytesRead += bytes;
142
+ },
143
+ });
144
+ this.performance.itemsParsed += page.linesParsed;
145
+ if (page.hasMore && page.returnedLines === 0) {
146
+ throw new errors_1.WebWorkbenchError(500, 'Git diff pagination could not advance.', 'git_pagination_stalled');
147
+ }
148
+ const after = await this.capture();
149
+ if (after.revision !== snapshot.revision) {
150
+ throw new errors_1.WebWorkbenchError(409, 'Repository changed while reading diff.', 'git_revision_conflict');
151
+ }
152
+ const binary = page.lines.some(line => /^(?:Binary files |GIT binary patch)/u.test(line));
153
+ const lines = binary ? page.lines.filter(line => !line.startsWith('literal ')) : page.lines;
154
+ return Object.freeze({
155
+ fileId: input.fileId,
156
+ path: (0, redaction_1.redactTraceText)(path),
157
+ repositoryRevision: snapshot.revision,
158
+ binary,
159
+ lines: Object.freeze(lines),
160
+ nextCursor: page.hasMore
161
+ ? this.encodeCursor({
162
+ version: 1,
163
+ kind: 'diff',
164
+ revision: snapshot.revision,
165
+ offset: offset + page.returnedLines,
166
+ fileId: input.fileId,
167
+ })
168
+ : null,
169
+ truncated: page.hasMore,
170
+ });
171
+ }
172
+ async capture() {
173
+ let root;
174
+ try {
175
+ root =
176
+ this.repositoryRoot ??
177
+ (0, fs_1.realpathSync)((await this.runGit(['rev-parse', '--show-toplevel'], this.workspace)).trim());
178
+ if (!isWithinRoot(root, this.workspace))
179
+ throw new Error('repository root escaped workspace');
180
+ this.repositoryRoot = root;
181
+ }
182
+ catch {
183
+ const revision = (0, crypto_1.createHash)('sha256').update(`not-git:${this.workspace}`).digest('hex');
184
+ return Object.freeze({
185
+ isRepository: false,
186
+ branch: null,
187
+ head: null,
188
+ upstream: null,
189
+ ahead: 0,
190
+ behind: 0,
191
+ records: Object.freeze([]),
192
+ rawStatus: '',
193
+ revision,
194
+ });
195
+ }
196
+ const [rawStatus, branchResult, headResult, upstreamResult] = await Promise.all([
197
+ this.runGit(['status', '--porcelain=v1', '-z', '--untracked-files=all'], root),
198
+ this.tryGit(['symbolic-ref', '--quiet', '--short', 'HEAD'], root),
199
+ this.tryGit(['rev-parse', 'HEAD'], root),
200
+ this.tryGit(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'], root),
201
+ ]);
202
+ const branch = branchResult?.trim() || null;
203
+ const head = headResult?.trim() || null;
204
+ const upstream = upstreamResult?.trim() || null;
205
+ let ahead = 0;
206
+ let behind = 0;
207
+ if (upstream) {
208
+ const counts = await this.tryGit(['rev-list', '--left-right', '--count', `HEAD...${upstream}`], root);
209
+ const [aheadValue, behindValue] = (counts ?? '').trim().split(/\s+/u).map(Number);
210
+ ahead = Number.isSafeInteger(aheadValue) ? aheadValue : 0;
211
+ behind = Number.isSafeInteger(behindValue) ? behindValue : 0;
212
+ }
213
+ const records = parsePorcelainV1(rawStatus);
214
+ this.performance.itemsParsed += records.length;
215
+ const fileFingerprints = records.map(record => fingerprintWorktreePath(root, record.path));
216
+ const revision = (0, crypto_1.createHash)('sha256')
217
+ .update(JSON.stringify({
218
+ root,
219
+ branch,
220
+ head,
221
+ upstream,
222
+ ahead,
223
+ behind,
224
+ rawStatus,
225
+ fileFingerprints,
226
+ }))
227
+ .digest('hex');
228
+ return Object.freeze({
229
+ isRepository: true,
230
+ root,
231
+ branch,
232
+ head,
233
+ upstream,
234
+ ahead,
235
+ behind,
236
+ records: Object.freeze(records),
237
+ rawStatus,
238
+ revision,
239
+ });
240
+ }
241
+ projectFile(record) {
242
+ return Object.freeze({
243
+ fileId: this.rememberPath(record.path),
244
+ path: (0, redaction_1.redactTraceText)(record.path),
245
+ indexStatus: record.indexStatus,
246
+ worktreeStatus: record.worktreeStatus,
247
+ ...(record.renamedFrom ? { renamedFrom: (0, redaction_1.redactTraceText)(record.renamedFrom) } : {}),
248
+ });
249
+ }
250
+ rememberPath(path) {
251
+ const existing = this.idByPath.get(path);
252
+ if (existing)
253
+ return existing;
254
+ const id = `git_${(0, crypto_1.createHmac)('sha256', this.secret)
255
+ .update(path)
256
+ .digest('base64url')
257
+ .slice(0, 32)}`;
258
+ this.idByPath.set(path, id);
259
+ this.pathById.set(id, path);
260
+ return id;
261
+ }
262
+ runGit(args, cwd) {
263
+ return new Promise((resolvePromise, reject) => {
264
+ this.performance.processCount += 1;
265
+ (0, child_process_1.execFile)('git', [...hardenedGitPrefix(), ...args], {
266
+ cwd,
267
+ encoding: 'utf8',
268
+ timeout: GIT_TIMEOUT_MS,
269
+ maxBuffer: GIT_MAX_BUFFER,
270
+ env: gitEnvironment(),
271
+ }, (error, stdout, stderr) => {
272
+ this.performance.bytesRead +=
273
+ Buffer.byteLength(stdout, 'utf8') + Buffer.byteLength(stderr, 'utf8');
274
+ if (error) {
275
+ const processError = error;
276
+ const timedOut = processError.code === 'ETIMEDOUT' ||
277
+ (processError.killed && processError.signal === 'SIGTERM');
278
+ const outputTooLarge = processError.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER' ||
279
+ /maxBuffer length exceeded/iu.test(processError.message);
280
+ reject(new errors_1.WebWorkbenchError(503, timedOut
281
+ ? 'Git command timed out.'
282
+ : outputTooLarge
283
+ ? 'Git output exceeded the bounded read limit.'
284
+ : (0, redaction_1.redactTraceText)(stderr || error.message || 'Git command failed.'), timedOut
285
+ ? 'git_timeout'
286
+ : outputTooLarge
287
+ ? 'git_output_too_large'
288
+ : 'git_command_failed'));
289
+ return;
290
+ }
291
+ resolvePromise(stdout);
292
+ });
293
+ });
294
+ }
295
+ async tryGit(args, cwd) {
296
+ try {
297
+ return await this.runGit(args, cwd);
298
+ }
299
+ catch {
300
+ return undefined;
301
+ }
302
+ }
303
+ encodeCursor(payload) {
304
+ const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
305
+ const signature = (0, crypto_1.createHmac)('sha256', this.secret).update(body).digest('base64url');
306
+ return `${body}.${signature}`;
307
+ }
308
+ decodeCursor(cursor, kind, revision, fileId) {
309
+ if (!cursor || cursor.length > 4096)
310
+ return invalidCursor();
311
+ const [body, encodedSignature, extra] = cursor.split('.');
312
+ if (!body || !encodedSignature || extra)
313
+ return invalidCursor();
314
+ const expected = (0, crypto_1.createHmac)('sha256', this.secret).update(body).digest();
315
+ let actual;
316
+ try {
317
+ actual = Buffer.from(encodedSignature, 'base64url');
318
+ }
319
+ catch {
320
+ return invalidCursor();
321
+ }
322
+ if (actual.length !== expected.length || !(0, crypto_1.timingSafeEqual)(actual, expected)) {
323
+ return invalidCursor();
324
+ }
325
+ try {
326
+ const parsed = JSON.parse(Buffer.from(body, 'base64url').toString('utf8'));
327
+ if (parsed.version !== 1 ||
328
+ parsed.kind !== kind ||
329
+ parsed.fileId !== fileId ||
330
+ !Number.isSafeInteger(parsed.offset) ||
331
+ parsed.offset < 0) {
332
+ return invalidCursor();
333
+ }
334
+ if (parsed.revision !== revision) {
335
+ throw new errors_1.WebWorkbenchError(409, 'Git cursor revision is stale.', 'git_revision_conflict');
336
+ }
337
+ return parsed;
338
+ }
339
+ catch (error) {
340
+ if (error instanceof errors_1.WebWorkbenchError)
341
+ throw error;
342
+ return invalidCursor();
343
+ }
344
+ }
345
+ }
346
+ exports.GitReadModelServiceV1 = GitReadModelServiceV1;
347
+ function parsePorcelainV1(output) {
348
+ const fields = output.split('\0');
349
+ const records = [];
350
+ for (let index = 0; index < fields.length; index += 1) {
351
+ const field = fields[index];
352
+ if (!field || field.length < 4)
353
+ continue;
354
+ const indexStatus = field[0];
355
+ const worktreeStatus = field[1];
356
+ const path = field.slice(3);
357
+ if (!safeGitPath(path))
358
+ continue;
359
+ const renamed = indexStatus === 'R' || indexStatus === 'C' || worktreeStatus === 'R';
360
+ const renamedFrom = renamed ? fields[++index] : undefined;
361
+ records.push(Object.freeze({
362
+ path,
363
+ indexStatus,
364
+ worktreeStatus,
365
+ ...(renamedFrom && safeGitPath(renamedFrom) ? { renamedFrom } : {}),
366
+ }));
367
+ }
368
+ return records.sort((left, right) => left.path.localeCompare(right.path));
369
+ }
370
+ function safeGitPath(path) {
371
+ return (Boolean(path) &&
372
+ path.length <= 4096 &&
373
+ !path.includes('\0') &&
374
+ !path.includes('\r') &&
375
+ !path.includes('\n') &&
376
+ !(0, path_1.isAbsolute)(path) &&
377
+ path !== '..' &&
378
+ !path.startsWith('../') &&
379
+ !path.includes('/../'));
380
+ }
381
+ function isConflict(file) {
382
+ return (file.indexStatus === 'U' ||
383
+ file.worktreeStatus === 'U' ||
384
+ `${file.indexStatus}${file.worktreeStatus}` === 'AA' ||
385
+ `${file.indexStatus}${file.worktreeStatus}` === 'DD');
386
+ }
387
+ function parseCommit(record) {
388
+ const [id, shortId, timestamp, authorName, subject] = record.split('\x1f');
389
+ if (!id || !shortId || !timestamp || !authorName || subject === undefined) {
390
+ throw new errors_1.WebWorkbenchError(502, 'Git log record is invalid.', 'git_output_invalid');
391
+ }
392
+ const authoredAt = new Date(Number(timestamp) * 1000);
393
+ if (!Number.isFinite(authoredAt.valueOf())) {
394
+ throw new errors_1.WebWorkbenchError(502, 'Git log timestamp is invalid.', 'git_output_invalid');
395
+ }
396
+ return Object.freeze({
397
+ id,
398
+ shortId,
399
+ authoredAt: authoredAt.toISOString(),
400
+ authorName: (0, redaction_1.redactTraceText)(authorName),
401
+ subject: (0, redaction_1.redactTraceText)(subject),
402
+ });
403
+ }
404
+ function diffCommands(record, path) {
405
+ const commands = [];
406
+ if (isConflictRecord(record) || (record.indexStatus !== ' ' && record.indexStatus !== '?')) {
407
+ commands.push({
408
+ title: 'Staged',
409
+ args: ['diff', '--cached', '--no-ext-diff', '--no-textconv', '--unified=3', '--', path],
410
+ });
411
+ }
412
+ if (isConflictRecord(record) ||
413
+ (record.worktreeStatus !== ' ' && record.worktreeStatus !== '?')) {
414
+ commands.push({
415
+ title: 'Working tree',
416
+ args: ['diff', '--no-ext-diff', '--no-textconv', '--unified=3', '--', path],
417
+ });
418
+ }
419
+ if (record.indexStatus === '?') {
420
+ commands.push({
421
+ title: 'Untracked',
422
+ args: [
423
+ 'diff',
424
+ '--no-index',
425
+ '--no-ext-diff',
426
+ '--no-textconv',
427
+ '--unified=3',
428
+ '--',
429
+ '/dev/null',
430
+ path,
431
+ ],
432
+ acceptedExitCodes: [0, 1],
433
+ });
434
+ }
435
+ return commands;
436
+ }
437
+ function isConflictRecord(record) {
438
+ return (record.indexStatus === 'U' ||
439
+ record.worktreeStatus === 'U' ||
440
+ `${record.indexStatus}${record.worktreeStatus}` === 'AA' ||
441
+ `${record.indexStatus}${record.worktreeStatus}` === 'DD');
442
+ }
443
+ async function streamDiffPage(input) {
444
+ const lines = [];
445
+ let virtualLine = 0;
446
+ let bytes = 0;
447
+ let hasMore = false;
448
+ let oversizedLine = false;
449
+ let linesParsed = 0;
450
+ const acceptLine = (raw) => {
451
+ linesParsed += 1;
452
+ const line = (0, redaction_1.redactTraceText)(raw.split(input.cwd).join('[WORKSPACE]'));
453
+ const lineBytes = Buffer.byteLength(line, 'utf8') + 1;
454
+ if (lineBytes > input.byteLimit) {
455
+ oversizedLine = true;
456
+ return false;
457
+ }
458
+ if (virtualLine < input.offset) {
459
+ virtualLine += 1;
460
+ return true;
461
+ }
462
+ if (lines.length >= input.lineLimit || bytes + lineBytes > input.byteLimit) {
463
+ hasMore = true;
464
+ return false;
465
+ }
466
+ lines.push(line);
467
+ bytes += lineBytes;
468
+ virtualLine += 1;
469
+ return true;
470
+ };
471
+ for (const command of input.commands) {
472
+ if (!acceptLine(`## ${command.title}`))
473
+ break;
474
+ const completed = await streamGitLines({
475
+ cwd: input.cwd,
476
+ args: command.args,
477
+ acceptedExitCodes: command.acceptedExitCodes ?? [0],
478
+ onLine: acceptLine,
479
+ onProcess: input.onProcess,
480
+ onBytes: input.onBytes,
481
+ });
482
+ if (!completed) {
483
+ hasMore = true;
484
+ break;
485
+ }
486
+ }
487
+ if (oversizedLine) {
488
+ throw new errors_1.WebWorkbenchError(413, 'A Git diff line exceeds the bounded page size.', 'git_line_too_long');
489
+ }
490
+ return Object.freeze({ lines, returnedLines: lines.length, hasMore, linesParsed });
491
+ }
492
+ function streamGitLines(input) {
493
+ return new Promise((resolvePromise, reject) => {
494
+ input.onProcess();
495
+ const child = (0, child_process_1.spawn)('git', [...hardenedGitPrefix(), ...input.args], {
496
+ cwd: input.cwd,
497
+ env: gitEnvironment(),
498
+ stdio: ['ignore', 'pipe', 'pipe'],
499
+ });
500
+ let buffer = '';
501
+ let stderr = '';
502
+ let outputBytes = 0;
503
+ let paginationStopped = false;
504
+ let timedOut = false;
505
+ let outputTooLarge = false;
506
+ let settled = false;
507
+ const timer = setTimeout(() => {
508
+ timedOut = true;
509
+ child.kill('SIGTERM');
510
+ }, GIT_TIMEOUT_MS);
511
+ timer.unref();
512
+ child.stdout.setEncoding('utf8');
513
+ child.stdout.on('data', (chunk) => {
514
+ input.onBytes(Buffer.byteLength(chunk, 'utf8'));
515
+ if (paginationStopped || timedOut || outputTooLarge)
516
+ return;
517
+ outputBytes += Buffer.byteLength(chunk, 'utf8');
518
+ if (outputBytes > GIT_MAX_BUFFER) {
519
+ outputTooLarge = true;
520
+ buffer = '';
521
+ clearTimeout(timer);
522
+ child.kill('SIGTERM');
523
+ return;
524
+ }
525
+ buffer += chunk;
526
+ for (;;) {
527
+ const newline = buffer.indexOf('\n');
528
+ if (newline < 0)
529
+ break;
530
+ const line = buffer.slice(0, newline).replace(/\r$/u, '');
531
+ buffer = buffer.slice(newline + 1);
532
+ if (!input.onLine(line)) {
533
+ paginationStopped = true;
534
+ clearTimeout(timer);
535
+ child.kill('SIGTERM');
536
+ break;
537
+ }
538
+ }
539
+ });
540
+ child.stderr.setEncoding('utf8');
541
+ child.stderr.on('data', (chunk) => {
542
+ input.onBytes(Buffer.byteLength(chunk, 'utf8'));
543
+ outputBytes += Buffer.byteLength(chunk, 'utf8');
544
+ if (outputBytes > GIT_MAX_BUFFER && !outputTooLarge) {
545
+ outputTooLarge = true;
546
+ clearTimeout(timer);
547
+ child.kill('SIGTERM');
548
+ }
549
+ if (stderr.length < 4096)
550
+ stderr += chunk.slice(0, 4096 - stderr.length);
551
+ });
552
+ child.once('error', error => {
553
+ if (settled)
554
+ return;
555
+ settled = true;
556
+ clearTimeout(timer);
557
+ reject(timedOut
558
+ ? new errors_1.WebWorkbenchError(503, 'Git command timed out.', 'git_timeout')
559
+ : outputTooLarge
560
+ ? new errors_1.WebWorkbenchError(503, 'Git output exceeded the bounded read limit.', 'git_output_too_large')
561
+ : new errors_1.WebWorkbenchError(503, (0, redaction_1.redactTraceText)(error.message), 'git_command_failed'));
562
+ });
563
+ child.once('close', code => {
564
+ if (settled)
565
+ return;
566
+ settled = true;
567
+ clearTimeout(timer);
568
+ if (timedOut) {
569
+ reject(new errors_1.WebWorkbenchError(503, 'Git command timed out.', 'git_timeout'));
570
+ return;
571
+ }
572
+ if (outputTooLarge) {
573
+ reject(new errors_1.WebWorkbenchError(503, 'Git output exceeded the bounded read limit.', 'git_output_too_large'));
574
+ return;
575
+ }
576
+ if (!paginationStopped && buffer && !input.onLine(buffer))
577
+ paginationStopped = true;
578
+ if (!paginationStopped && !input.acceptedExitCodes.includes(code ?? -1)) {
579
+ reject(new errors_1.WebWorkbenchError(503, (0, redaction_1.redactTraceText)(stderr || 'Git diff command failed.'), 'git_command_failed'));
580
+ return;
581
+ }
582
+ resolvePromise(!paginationStopped);
583
+ });
584
+ });
585
+ }
586
+ function gitEnvironment() {
587
+ return {
588
+ PATH: process.env.PATH,
589
+ HOME: process.env.HOME,
590
+ LANG: 'C.UTF-8',
591
+ LC_ALL: 'C.UTF-8',
592
+ GIT_TERMINAL_PROMPT: '0',
593
+ GIT_OPTIONAL_LOCKS: '0',
594
+ GIT_CONFIG_NOSYSTEM: '1',
595
+ GIT_CONFIG_GLOBAL: GIT_NULL_DEVICE,
596
+ GIT_PAGER: 'cat',
597
+ PAGER: 'cat',
598
+ };
599
+ }
600
+ function hardenedGitPrefix() {
601
+ return [
602
+ '-c',
603
+ 'core.quotepath=false',
604
+ '-c',
605
+ 'core.fsmonitor=false',
606
+ '-c',
607
+ `core.hooksPath=${GIT_NULL_DEVICE}`,
608
+ '-c',
609
+ 'log.showSignature=false',
610
+ '--literal-pathspecs',
611
+ ];
612
+ }
613
+ function canonicalDirectory(path) {
614
+ try {
615
+ const canonical = (0, fs_1.realpathSync)((0, path_1.resolve)(path));
616
+ if (!(0, fs_1.statSync)(canonical).isDirectory())
617
+ throw new Error('not a directory');
618
+ return canonical;
619
+ }
620
+ catch {
621
+ throw new errors_1.WebWorkbenchError(400, 'Workspace directory is unavailable.');
622
+ }
623
+ }
624
+ function fingerprintWorktreePath(root, path) {
625
+ const candidate = (0, path_1.resolve)(root, path);
626
+ if (!isWithinRoot(candidate, root))
627
+ return Object.freeze([path, 'outside']);
628
+ try {
629
+ const stat = (0, fs_1.lstatSync)(candidate, { bigint: true });
630
+ return Object.freeze([
631
+ path,
632
+ stat.dev.toString(),
633
+ stat.ino.toString(),
634
+ stat.mode.toString(),
635
+ stat.size.toString(),
636
+ stat.mtimeNs.toString(),
637
+ stat.ctimeNs.toString(),
638
+ ]);
639
+ }
640
+ catch {
641
+ return Object.freeze([path, 'missing']);
642
+ }
643
+ }
644
+ function isWithinRoot(candidate, root) {
645
+ const rel = (0, path_1.relative)(root, candidate);
646
+ return rel === '' || (!rel.startsWith(`..${path_1.sep}`) && rel !== '..' && !(0, path_1.isAbsolute)(rel));
647
+ }
648
+ function boundedInteger(value, min, max, name) {
649
+ if (!Number.isSafeInteger(value) || value < min || value > max) {
650
+ throw new errors_1.WebWorkbenchError(400, `${name} must be an integer from ${min} through ${max}.`);
651
+ }
652
+ return value;
653
+ }
654
+ function invalidCursor() {
655
+ throw new errors_1.WebWorkbenchError(400, 'Git cursor is invalid.', 'git_cursor_invalid');
656
+ }
657
+ //# sourceMappingURL=git-read-model-service.js.map