opendoor-plugin 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/LICENSE +21 -0
- package/README.md +299 -0
- package/dist/browser/pi-web-plugin.js +9499 -0
- package/dist/package.json +52 -0
- package/dist/server-plugin.js +1047 -0
- package/package.json +52 -0
|
@@ -0,0 +1,1047 @@
|
|
|
1
|
+
// Generated from src/server-plugin.ts. Do not edit directly.
|
|
2
|
+
|
|
3
|
+
// src/server-plugin.ts
|
|
4
|
+
import { readdir as readdir2 } from "node:fs/promises";
|
|
5
|
+
import { join as join2 } from "node:path";
|
|
6
|
+
|
|
7
|
+
// src/doorstop-backend.ts
|
|
8
|
+
import { readdir } from "node:fs/promises";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
// src/doorstop-contract.ts
|
|
12
|
+
function formatUnknownError(error) {
|
|
13
|
+
return error instanceof Error ? error.message : String(error);
|
|
14
|
+
}
|
|
15
|
+
function isRecord(value) {
|
|
16
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
function requireRecord(value, label) {
|
|
19
|
+
if (!isRecord(value)) throw new Error(`${label} must be an object`);
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function requireArrayValue(value, label) {
|
|
23
|
+
if (!Array.isArray(value)) throw new Error(`${label} must be an array`);
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function requireString(record, key) {
|
|
27
|
+
const value = record[key];
|
|
28
|
+
if (typeof value !== "string") throw new Error(`Expected string field: ${key}`);
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/doorstop-backend-contract.ts
|
|
33
|
+
var DOORSTOP_RUN_OPERATION = "doorstop.run";
|
|
34
|
+
var DOORSTOP_BASELINE_OPERATION = "doorstop.item-baseline";
|
|
35
|
+
var DOORSTOP_BASELINE_GREP_LIMIT = 20;
|
|
36
|
+
var DOORSTOP_BASELINE_HISTORY_LIMIT = 50;
|
|
37
|
+
var DOORSTOP_BASELINE_BLOB_MAX = 256 * 1024;
|
|
38
|
+
function isValidDoorstopUid(value) {
|
|
39
|
+
if (value === "") return false;
|
|
40
|
+
if (!/^[\w.-]+$/.test(value)) return false;
|
|
41
|
+
if (/^[\w.-]+[-_.][\w]+$/.test(value)) return true;
|
|
42
|
+
if (/^[\w.-]*\D\d+$/.test(value)) return true;
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
function isValidDoorstopItemPath(value) {
|
|
46
|
+
if (value === "") return false;
|
|
47
|
+
if (value.length > 256) return false;
|
|
48
|
+
if (value.startsWith("/")) return false;
|
|
49
|
+
if (/^[A-Za-z]:/.test(value)) return false;
|
|
50
|
+
if (value.includes("\\")) return false;
|
|
51
|
+
if (/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/.test(value)) return false;
|
|
52
|
+
for (const segment of value.split("/")) {
|
|
53
|
+
if (segment === "..") return false;
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
function isValidPublishTarget(value) {
|
|
58
|
+
if (value === "") return false;
|
|
59
|
+
if (value.startsWith("-")) return false;
|
|
60
|
+
if (value.startsWith("/")) return false;
|
|
61
|
+
if (/^[A-Za-z]:/.test(value)) return false;
|
|
62
|
+
if (value.includes("\\")) return false;
|
|
63
|
+
for (const segment of value.split("/")) {
|
|
64
|
+
if (segment === "..") return false;
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
var DOORSTOP_RUN_OPS = /* @__PURE__ */ new Set([
|
|
69
|
+
"validate",
|
|
70
|
+
"publish",
|
|
71
|
+
"review",
|
|
72
|
+
"clear",
|
|
73
|
+
"edit",
|
|
74
|
+
"link",
|
|
75
|
+
"unlink"
|
|
76
|
+
]);
|
|
77
|
+
function parseDoorstopRunOp(value) {
|
|
78
|
+
if (!DOORSTOP_RUN_OPS.has(value)) {
|
|
79
|
+
throw new Error(`Invalid doorstop run op: ${JSON.stringify(value)}`);
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
function requireStringArray(record, key) {
|
|
84
|
+
const values = requireArrayValue(record[key], key);
|
|
85
|
+
if (!values.every((entry) => typeof entry === "string")) {
|
|
86
|
+
throw new Error(`Expected string array field: ${key}`);
|
|
87
|
+
}
|
|
88
|
+
return values;
|
|
89
|
+
}
|
|
90
|
+
function optionalBoolean(record, key) {
|
|
91
|
+
const value = record[key];
|
|
92
|
+
if (value === void 0) return void 0;
|
|
93
|
+
if (typeof value !== "boolean") throw new Error(`Expected boolean field: ${key}`);
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
function requireUid(record, key) {
|
|
97
|
+
const value = requireString(record, key);
|
|
98
|
+
if (!isValidDoorstopUid(value)) {
|
|
99
|
+
throw new Error(`Invalid doorstop UID in field: ${key}`);
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
function requireItemPath(record, key) {
|
|
104
|
+
const value = requireString(record, key);
|
|
105
|
+
if (!isValidDoorstopItemPath(value)) {
|
|
106
|
+
throw new Error(`Invalid doorstop item path in field: ${key}`);
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
function parseDoorstopRunRequest(value) {
|
|
111
|
+
const record = requireRecord(value, "Doorstop run request");
|
|
112
|
+
const op = parseDoorstopRunOp(requireString(record, "op"));
|
|
113
|
+
switch (op) {
|
|
114
|
+
case "validate":
|
|
115
|
+
return { op };
|
|
116
|
+
case "publish": {
|
|
117
|
+
const target = requireString(record, "target");
|
|
118
|
+
if (!isValidPublishTarget(target)) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
'Invalid publish target in doorstop.run request \u2014 must be a workspace-relative path (never absolute, never a drive-letter prefix, no ".." segment)'
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return { op, target };
|
|
124
|
+
}
|
|
125
|
+
case "review": {
|
|
126
|
+
const uid = requireUid(record, "uid");
|
|
127
|
+
const commit = optionalBoolean(record, "commit");
|
|
128
|
+
return commit === void 0 ? { op, uid } : { op, uid, commit };
|
|
129
|
+
}
|
|
130
|
+
case "clear": {
|
|
131
|
+
const uid = requireUid(record, "uid");
|
|
132
|
+
const parents = requireStringArray(record, "parents");
|
|
133
|
+
if (parents.length === 0) {
|
|
134
|
+
throw new Error("Invalid doorstop.run clear request: at least one parent UID is required");
|
|
135
|
+
}
|
|
136
|
+
for (const parent of parents) {
|
|
137
|
+
if (!isValidDoorstopUid(parent)) throw new Error(`Invalid doorstop UID in field: parents`);
|
|
138
|
+
}
|
|
139
|
+
return { op, uid, parents };
|
|
140
|
+
}
|
|
141
|
+
case "edit":
|
|
142
|
+
return { op, uid: requireUid(record, "uid") };
|
|
143
|
+
case "link":
|
|
144
|
+
case "unlink": {
|
|
145
|
+
const uid = requireUid(record, "uid");
|
|
146
|
+
const target = requireUid(record, "target");
|
|
147
|
+
return { op, uid, target };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function parseDoorstopBaselineRequest(value) {
|
|
152
|
+
const record = requireRecord(value, "Doorstop baseline request");
|
|
153
|
+
return {
|
|
154
|
+
uid: requireUid(record, "uid"),
|
|
155
|
+
path: requireItemPath(record, "path")
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
var DOORSTOP_GIT_STATUS_OPERATION = "doorstop.git-status";
|
|
159
|
+
var DOORSTOP_GIT_STATUS_FILES_MAX = 200;
|
|
160
|
+
function parseDoorstopGitStatusRequest(value) {
|
|
161
|
+
requireRecord(value, "Doorstop git status request");
|
|
162
|
+
return {};
|
|
163
|
+
}
|
|
164
|
+
var DOORSTOP_GIT_STAGE_OPERATION = "doorstop.git-stage";
|
|
165
|
+
function parseDoorstopGitStageRequest(value) {
|
|
166
|
+
const record = requireRecord(value, "Doorstop git stage request");
|
|
167
|
+
const paths = requireStringArray(record, "paths");
|
|
168
|
+
if (paths.length === 0) {
|
|
169
|
+
throw new Error("Invalid doorstop.git-stage request: at least one path is required");
|
|
170
|
+
}
|
|
171
|
+
const seen = /* @__PURE__ */ new Set();
|
|
172
|
+
const uniquePaths = [];
|
|
173
|
+
for (const path of paths) {
|
|
174
|
+
if (!isValidDoorstopItemPath(path)) {
|
|
175
|
+
throw new Error(`Invalid doorstop item path in field: paths`);
|
|
176
|
+
}
|
|
177
|
+
if (!seen.has(path)) {
|
|
178
|
+
seen.add(path);
|
|
179
|
+
uniquePaths.push(path);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { paths: uniquePaths };
|
|
183
|
+
}
|
|
184
|
+
var DOORSTOP_GIT_UNSTAGE_OPERATION = "doorstop.git-unstage";
|
|
185
|
+
function parseDoorstopGitUnstageRequest(value) {
|
|
186
|
+
const record = requireRecord(value, "Doorstop git unstage request");
|
|
187
|
+
const paths = requireStringArray(record, "paths");
|
|
188
|
+
if (paths.length === 0) {
|
|
189
|
+
throw new Error("Invalid doorstop.git-unstage request: at least one path is required");
|
|
190
|
+
}
|
|
191
|
+
const seen = /* @__PURE__ */ new Set();
|
|
192
|
+
const uniquePaths = [];
|
|
193
|
+
for (const path of paths) {
|
|
194
|
+
if (!isValidDoorstopItemPath(path)) {
|
|
195
|
+
throw new Error(`Invalid doorstop item path in field: paths`);
|
|
196
|
+
}
|
|
197
|
+
if (!seen.has(path)) {
|
|
198
|
+
seen.add(path);
|
|
199
|
+
uniquePaths.push(path);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return { paths: uniquePaths };
|
|
203
|
+
}
|
|
204
|
+
var DOORSTOP_GIT_COMMIT_OPERATION = "doorstop.git-commit";
|
|
205
|
+
function isValidGitCommitMessage(value) {
|
|
206
|
+
if (value === "") return false;
|
|
207
|
+
if (value.length > 2e3) return false;
|
|
208
|
+
if (value.trim() === "") return false;
|
|
209
|
+
if (/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/.test(value)) return false;
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
function parseDoorstopGitCommitRequest(value) {
|
|
213
|
+
const record = requireRecord(value, "Doorstop git commit request");
|
|
214
|
+
const message = requireString(record, "message");
|
|
215
|
+
if (!isValidGitCommitMessage(message)) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
"Invalid git commit message \u2014 must be a non-blank single line of at most 2000 characters"
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
return { message: message.trim() };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/doorstop-backend.ts
|
|
224
|
+
var DEFAULT_DOORSTOP_COMMAND = "doorstop";
|
|
225
|
+
var DEFAULT_GIT_COMMAND = "git";
|
|
226
|
+
var DEFAULT_DOORSTOP_TIMEOUT_MS = 8500;
|
|
227
|
+
var DOORSTOP_UNSET_ENV_KEYS = Object.freeze(["DOORSTOP_HOME", "PYTHONPATH"]);
|
|
228
|
+
var DOORSTOP_CLI_NOT_FOUND_MESSAGE = "opendoor: doorstop CLI not found on the sessiond host PATH \u2014 configure plugins.opendoor.settings.doorstopPath";
|
|
229
|
+
var GIT_NOT_FOUND_MESSAGE = "opendoor: git not found on the sessiond host PATH \u2014 configure plugins.opendoor.settings.gitPath";
|
|
230
|
+
var PIPELINE_DEADLINE_BUDGET_MS = 9500;
|
|
231
|
+
var COMMIT_STDERR_EXCERPT_LIMIT = 2048;
|
|
232
|
+
var GIT_UNSET_ENV_KEYS = Object.freeze([
|
|
233
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
234
|
+
"GIT_COMMON_DIR",
|
|
235
|
+
"GIT_DIR",
|
|
236
|
+
"GIT_INDEX_FILE",
|
|
237
|
+
"GIT_OBJECT_DIRECTORY",
|
|
238
|
+
"GIT_PREFIX",
|
|
239
|
+
"GIT_QUARANTINE_PATH",
|
|
240
|
+
"GIT_WORK_TREE"
|
|
241
|
+
]);
|
|
242
|
+
var ITEM_PATH_WALK_MAX_DEPTH = 12;
|
|
243
|
+
var ITEM_PATH_WALK_MAX_ENTRIES = 2048;
|
|
244
|
+
var ITEM_PATH_WALK_SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".pi-web"]);
|
|
245
|
+
var activationRunQueues = /* @__PURE__ */ new WeakMap();
|
|
246
|
+
async function requestDoorstopBackend(context, request) {
|
|
247
|
+
if (request.operation !== DOORSTOP_RUN_OPERATION) {
|
|
248
|
+
throw unsupportedBackendOperationError(request.operation);
|
|
249
|
+
}
|
|
250
|
+
const run = parseDoorstopRunRequest(request.input);
|
|
251
|
+
const settings = parseDoorstopBackendSettings(context.settings);
|
|
252
|
+
const cwd = request.workspace.path;
|
|
253
|
+
return runSerialized(context, cwd, async () => {
|
|
254
|
+
const startedAt = Date.now();
|
|
255
|
+
const result = await runDoorstop(
|
|
256
|
+
context,
|
|
257
|
+
settings,
|
|
258
|
+
cwd,
|
|
259
|
+
doorstopArgv(run),
|
|
260
|
+
request.signal,
|
|
261
|
+
Math.min(settings.timeoutMs, remainingBudgetMs(startedAt))
|
|
262
|
+
);
|
|
263
|
+
let commit;
|
|
264
|
+
if (run.op === "review" && run.commit === true) {
|
|
265
|
+
commit = result.exitCode === 0 ? await reviewCommitPipeline(context, settings, cwd, run.uid, request.signal, startedAt) : { status: "skipped" };
|
|
266
|
+
}
|
|
267
|
+
return doorstopRunResponse(run.op, result, Date.now() - startedAt, commit);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
function doorstopArgv(run) {
|
|
271
|
+
switch (run.op) {
|
|
272
|
+
case "validate":
|
|
273
|
+
return [];
|
|
274
|
+
case "publish":
|
|
275
|
+
return ["publish", "all", run.target];
|
|
276
|
+
case "review":
|
|
277
|
+
return ["review", run.uid];
|
|
278
|
+
case "clear":
|
|
279
|
+
return ["clear", run.uid, ...run.parents];
|
|
280
|
+
case "edit":
|
|
281
|
+
return ["edit", run.uid];
|
|
282
|
+
case "link":
|
|
283
|
+
return ["link", run.uid, run.target];
|
|
284
|
+
case "unlink":
|
|
285
|
+
return ["unlink", run.uid, run.target];
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async function runDoorstop(context, settings, cwd, args, signal, timeoutMs = settings.timeoutMs) {
|
|
289
|
+
try {
|
|
290
|
+
return await context.execFile({
|
|
291
|
+
file: settings.doorstopPath,
|
|
292
|
+
args,
|
|
293
|
+
cwd,
|
|
294
|
+
unsetEnv: DOORSTOP_UNSET_ENV_KEYS,
|
|
295
|
+
timeoutMs,
|
|
296
|
+
signal
|
|
297
|
+
});
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (isEnoentError(error)) {
|
|
300
|
+
throw new Error(DOORSTOP_CLI_NOT_FOUND_MESSAGE);
|
|
301
|
+
}
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function doorstopRunResponse(op, result, durationMs, commit) {
|
|
306
|
+
return {
|
|
307
|
+
op,
|
|
308
|
+
exitCode: result.exitCode,
|
|
309
|
+
signal: result.signal,
|
|
310
|
+
stdout: result.stdout,
|
|
311
|
+
stderr: result.stderr,
|
|
312
|
+
stdoutTruncated: result.stdoutTruncated,
|
|
313
|
+
stderrTruncated: result.stderrTruncated,
|
|
314
|
+
durationMs,
|
|
315
|
+
// Omitted unless the pipeline produced one (exactOptionalPropertyTypes:
|
|
316
|
+
// a response without `commit` must not carry the key at all). The spread
|
|
317
|
+
// yields a fresh object so the value admits the host bridge's JsonObject
|
|
318
|
+
// index signature (the interface itself lacks it).
|
|
319
|
+
...commit === void 0 ? {} : { commit: { ...commit } }
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
async function runSerialized(context, cwd, action) {
|
|
323
|
+
let queues = activationRunQueues.get(context);
|
|
324
|
+
if (queues === void 0) {
|
|
325
|
+
queues = /* @__PURE__ */ new Map();
|
|
326
|
+
activationRunQueues.set(context, queues);
|
|
327
|
+
}
|
|
328
|
+
const previous = queues.get(cwd) ?? Promise.resolve();
|
|
329
|
+
const run = previous.then(action, action);
|
|
330
|
+
const tail = run.then(
|
|
331
|
+
() => void 0,
|
|
332
|
+
() => void 0
|
|
333
|
+
);
|
|
334
|
+
queues.set(cwd, tail);
|
|
335
|
+
try {
|
|
336
|
+
return await run;
|
|
337
|
+
} finally {
|
|
338
|
+
if (queues.get(cwd) === tail) queues.delete(cwd);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function parseDoorstopBackendSettings(settings) {
|
|
342
|
+
const configuredPath = settings["doorstopPath"];
|
|
343
|
+
const doorstopPath = typeof configuredPath === "string" && configuredPath !== "" ? configuredPath : DEFAULT_DOORSTOP_COMMAND;
|
|
344
|
+
const configuredGitPath = settings["gitPath"];
|
|
345
|
+
const gitPath = typeof configuredGitPath === "string" && configuredGitPath !== "" ? configuredGitPath : DEFAULT_GIT_COMMAND;
|
|
346
|
+
const configuredTimeout = settings["timeoutMs"];
|
|
347
|
+
const timeoutMs = typeof configuredTimeout === "number" && Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? Math.min(configuredTimeout, DEFAULT_DOORSTOP_TIMEOUT_MS) : DEFAULT_DOORSTOP_TIMEOUT_MS;
|
|
348
|
+
return { doorstopPath, gitPath, timeoutMs };
|
|
349
|
+
}
|
|
350
|
+
function isEnoentError(error) {
|
|
351
|
+
return typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
352
|
+
}
|
|
353
|
+
function unsupportedBackendOperationError(operation) {
|
|
354
|
+
return new Error(`opendoor: unsupported workspace backend operation: ${operation}`);
|
|
355
|
+
}
|
|
356
|
+
function remainingBudgetMs(startedAt) {
|
|
357
|
+
return PIPELINE_DEADLINE_BUDGET_MS - (Date.now() - startedAt);
|
|
358
|
+
}
|
|
359
|
+
function gitExecTimeoutMs(settings, startedAt) {
|
|
360
|
+
return Math.max(1, Math.min(settings.timeoutMs, remainingBudgetMs(startedAt)));
|
|
361
|
+
}
|
|
362
|
+
function commitStderrExcerpt(stderr) {
|
|
363
|
+
return stderr.length > COMMIT_STDERR_EXCERPT_LIMIT ? `${stderr.slice(0, COMMIT_STDERR_EXCERPT_LIMIT)}
|
|
364
|
+
\u2026 (stderr truncated)` : stderr;
|
|
365
|
+
}
|
|
366
|
+
async function runGit(context, settings, cwd, args, signal, timeoutMs) {
|
|
367
|
+
try {
|
|
368
|
+
return await context.execFile({
|
|
369
|
+
file: settings.gitPath,
|
|
370
|
+
args,
|
|
371
|
+
cwd,
|
|
372
|
+
unsetEnv: GIT_UNSET_ENV_KEYS,
|
|
373
|
+
timeoutMs,
|
|
374
|
+
signal
|
|
375
|
+
});
|
|
376
|
+
} catch (error) {
|
|
377
|
+
if (isEnoentError(error)) {
|
|
378
|
+
throw new Error(GIT_NOT_FOUND_MESSAGE);
|
|
379
|
+
}
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function isWorkTreeResult(result) {
|
|
384
|
+
return result.exitCode === 0 && result.stdout.split(/\r?\n/, 1)[0] === "true";
|
|
385
|
+
}
|
|
386
|
+
function commitBracketSha(commitStdout) {
|
|
387
|
+
const firstLine = commitStdout.split(/\r?\n/, 1)[0] ?? "";
|
|
388
|
+
const match = /^\[.*\b([0-9a-f]{7,40})\](?:.*)$/.exec(firstLine);
|
|
389
|
+
return match?.[1];
|
|
390
|
+
}
|
|
391
|
+
function gitStepFailure(result, args) {
|
|
392
|
+
const stderr = result.stderr.trim();
|
|
393
|
+
const reason = stderr !== "" ? stderr : result.signal !== null ? `git ${args.join(" ")} ended from signal ${result.signal}` : `git ${args.join(" ")} exited ${String(result.exitCode)}`;
|
|
394
|
+
throw new Error(reason);
|
|
395
|
+
}
|
|
396
|
+
async function reviewCommitPipeline(context, settings, cwd, uid, signal, startedAt) {
|
|
397
|
+
try {
|
|
398
|
+
if (remainingBudgetMs(startedAt) <= 0) return { status: "skipped" };
|
|
399
|
+
const inWorkTree = await runGit(
|
|
400
|
+
context,
|
|
401
|
+
settings,
|
|
402
|
+
cwd,
|
|
403
|
+
["rev-parse", "--is-inside-work-tree"],
|
|
404
|
+
signal,
|
|
405
|
+
gitExecTimeoutMs(settings, startedAt)
|
|
406
|
+
);
|
|
407
|
+
if (!isWorkTreeResult(inWorkTree)) return { status: "skipped" };
|
|
408
|
+
if (remainingBudgetMs(startedAt) <= 0) return { status: "skipped" };
|
|
409
|
+
const itemPath = await findItemPath(cwd, uid);
|
|
410
|
+
if (itemPath === void 0 || !isValidDoorstopItemPath(itemPath)) return { status: "skipped" };
|
|
411
|
+
if (remainingBudgetMs(startedAt) <= 0) return { status: "skipped" };
|
|
412
|
+
const status = await runGit(
|
|
413
|
+
context,
|
|
414
|
+
settings,
|
|
415
|
+
cwd,
|
|
416
|
+
["status", "--porcelain", "--", literalPathspec(itemPath)],
|
|
417
|
+
signal,
|
|
418
|
+
gitExecTimeoutMs(settings, startedAt)
|
|
419
|
+
);
|
|
420
|
+
if (status.exitCode !== 0) gitStepFailure(status, ["status", "--porcelain", "--", literalPathspec(itemPath)]);
|
|
421
|
+
if (status.stdout === "") return { status: "clean" };
|
|
422
|
+
if (remainingBudgetMs(startedAt) <= 0) return { status: "skipped" };
|
|
423
|
+
const add = await runGit(context, settings, cwd, ["add", "--", literalPathspec(itemPath)], signal, gitExecTimeoutMs(settings, startedAt));
|
|
424
|
+
if (add.exitCode !== 0) gitStepFailure(add, ["add", "--", literalPathspec(itemPath)]);
|
|
425
|
+
if (remainingBudgetMs(startedAt) <= 0) return { status: "skipped" };
|
|
426
|
+
const commit = await runGit(
|
|
427
|
+
context,
|
|
428
|
+
settings,
|
|
429
|
+
cwd,
|
|
430
|
+
["commit", "-m", `doorstop: review ${uid}`, "--", literalPathspec(itemPath)],
|
|
431
|
+
signal,
|
|
432
|
+
gitExecTimeoutMs(settings, startedAt)
|
|
433
|
+
);
|
|
434
|
+
if (commit.exitCode !== 0)
|
|
435
|
+
gitStepFailure(commit, ["commit", "-m", `doorstop: review ${uid}`, "--", literalPathspec(itemPath)]);
|
|
436
|
+
if (remainingBudgetMs(startedAt) <= 0) return { status: "skipped" };
|
|
437
|
+
const head = await runGit(
|
|
438
|
+
context,
|
|
439
|
+
settings,
|
|
440
|
+
cwd,
|
|
441
|
+
["rev-parse", "--short", "HEAD"],
|
|
442
|
+
signal,
|
|
443
|
+
gitExecTimeoutMs(settings, startedAt)
|
|
444
|
+
);
|
|
445
|
+
if (head.exitCode !== 0) gitStepFailure(head, ["rev-parse", "--short", "HEAD"]);
|
|
446
|
+
const sha = head.stdout.trim();
|
|
447
|
+
if (sha === "") gitStepFailure(head, ["rev-parse", "--short", "HEAD"]);
|
|
448
|
+
return { status: "committed", sha };
|
|
449
|
+
} catch (error) {
|
|
450
|
+
if (signal.aborted) throw error;
|
|
451
|
+
return { status: "failed", stderr: commitStderrExcerpt(formatUnknownError(error)) };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async function requestDoorstopBaseline(context, request) {
|
|
455
|
+
if (request.operation !== DOORSTOP_BASELINE_OPERATION) {
|
|
456
|
+
throw unsupportedBackendOperationError(request.operation);
|
|
457
|
+
}
|
|
458
|
+
const baseline = parseDoorstopBaselineRequest(request.input);
|
|
459
|
+
const settings = parseDoorstopBackendSettings(context.settings);
|
|
460
|
+
const cwd = request.workspace.path;
|
|
461
|
+
return runSerialized(context, cwd, async () => {
|
|
462
|
+
const startedAt = Date.now();
|
|
463
|
+
const inWorkTree = await baselineGit(
|
|
464
|
+
context,
|
|
465
|
+
settings,
|
|
466
|
+
cwd,
|
|
467
|
+
["rev-parse", "--is-inside-work-tree", "--show-prefix"],
|
|
468
|
+
request.signal,
|
|
469
|
+
startedAt
|
|
470
|
+
);
|
|
471
|
+
if (inWorkTree === void 0 || !isWorkTreeResult(inWorkTree)) {
|
|
472
|
+
return baselineResponse(false, "none", []);
|
|
473
|
+
}
|
|
474
|
+
const showPrefix = inWorkTree.stdout.split(/\r?\n/)[1] ?? "";
|
|
475
|
+
const pattern = `^doorstop: review ${escapeRegexMeta(baseline.uid)}$`;
|
|
476
|
+
const grep = await baselineGit(
|
|
477
|
+
context,
|
|
478
|
+
settings,
|
|
479
|
+
cwd,
|
|
480
|
+
[
|
|
481
|
+
"log",
|
|
482
|
+
`--max-count=${DOORSTOP_BASELINE_GREP_LIMIT}`,
|
|
483
|
+
`--grep=${pattern}`,
|
|
484
|
+
"--format=%H",
|
|
485
|
+
"--",
|
|
486
|
+
literalPathspec(baseline.path)
|
|
487
|
+
],
|
|
488
|
+
request.signal,
|
|
489
|
+
startedAt
|
|
490
|
+
);
|
|
491
|
+
let shas;
|
|
492
|
+
let source;
|
|
493
|
+
if (grep !== void 0 && grep.exitCode === 0 && grep.stdout.trim() !== "") {
|
|
494
|
+
shas = grep.stdout.trim().split(/\r?\n/);
|
|
495
|
+
source = "review-commit";
|
|
496
|
+
} else {
|
|
497
|
+
const history = await baselineGit(
|
|
498
|
+
context,
|
|
499
|
+
settings,
|
|
500
|
+
cwd,
|
|
501
|
+
["log", `--max-count=${DOORSTOP_BASELINE_HISTORY_LIMIT}`, "--format=%H", "--", literalPathspec(baseline.path)],
|
|
502
|
+
request.signal,
|
|
503
|
+
startedAt
|
|
504
|
+
);
|
|
505
|
+
if (history === void 0 || history.exitCode !== 0 || history.stdout.trim() === "") {
|
|
506
|
+
return baselineResponse(true, "none", []);
|
|
507
|
+
}
|
|
508
|
+
shas = history.stdout.trim().split(/\r?\n/);
|
|
509
|
+
source = "history";
|
|
510
|
+
}
|
|
511
|
+
const candidates = [];
|
|
512
|
+
for (const sha of shas) {
|
|
513
|
+
if (sha === "" || remainingBudgetMs(startedAt) <= 0) break;
|
|
514
|
+
const show = await baselineGit(
|
|
515
|
+
context,
|
|
516
|
+
settings,
|
|
517
|
+
cwd,
|
|
518
|
+
["show", `${sha}:${showPrefix}${stripLeadingDotSlash(baseline.path)}`],
|
|
519
|
+
request.signal,
|
|
520
|
+
startedAt
|
|
521
|
+
);
|
|
522
|
+
if (show === void 0 || show.exitCode !== 0) continue;
|
|
523
|
+
if (show.stdout.length > DOORSTOP_BASELINE_BLOB_MAX) continue;
|
|
524
|
+
candidates.push({ sha, blob: show.stdout });
|
|
525
|
+
}
|
|
526
|
+
return baselineResponse(true, source, candidates);
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
async function baselineGit(context, settings, cwd, args, signal, startedAt) {
|
|
530
|
+
if (remainingBudgetMs(startedAt) <= 0) return void 0;
|
|
531
|
+
try {
|
|
532
|
+
return await runGit(context, settings, cwd, args, signal, gitExecTimeoutMs(settings, startedAt));
|
|
533
|
+
} catch {
|
|
534
|
+
return void 0;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
function baselineResponse(git, source, candidates) {
|
|
538
|
+
return {
|
|
539
|
+
git,
|
|
540
|
+
source,
|
|
541
|
+
// Each candidate is spread into a fresh object so the value admits the
|
|
542
|
+
// host bridge's JsonObject index signature.
|
|
543
|
+
candidates: candidates.map((candidate) => ({ ...candidate }))
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
function escapeRegexMeta(value) {
|
|
547
|
+
return value.replace(/[.*\[\]^$\\]/g, "\\$&");
|
|
548
|
+
}
|
|
549
|
+
function literalPathspec(path) {
|
|
550
|
+
return `:(literal)${path}`;
|
|
551
|
+
}
|
|
552
|
+
function stripLeadingDotSlash(path) {
|
|
553
|
+
return path.replace(/^\.\//, "");
|
|
554
|
+
}
|
|
555
|
+
async function findItemPath(root, uid) {
|
|
556
|
+
let scanned = 0;
|
|
557
|
+
const walk = async (directory, depth, relPath) => {
|
|
558
|
+
if (scanned >= ITEM_PATH_WALK_MAX_ENTRIES || depth > ITEM_PATH_WALK_MAX_DEPTH) return void 0;
|
|
559
|
+
scanned += 1;
|
|
560
|
+
let entries;
|
|
561
|
+
try {
|
|
562
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
563
|
+
} catch {
|
|
564
|
+
return void 0;
|
|
565
|
+
}
|
|
566
|
+
for (const entry of entries) {
|
|
567
|
+
if (scanned >= ITEM_PATH_WALK_MAX_ENTRIES) return void 0;
|
|
568
|
+
scanned += 1;
|
|
569
|
+
if (entry.isDirectory()) {
|
|
570
|
+
if (!ITEM_PATH_WALK_SKIP_DIRS.has(entry.name)) {
|
|
571
|
+
const childRel = relPath === "" ? entry.name : `${relPath}/${entry.name}`;
|
|
572
|
+
const found = await walk(join(directory, entry.name), depth + 1, childRel);
|
|
573
|
+
if (found !== void 0) return found;
|
|
574
|
+
}
|
|
575
|
+
} else if (entry.isFile() && itemFileBase(entry.name) === uid) {
|
|
576
|
+
return relPath === "" ? entry.name : `${relPath}/${entry.name}`;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return void 0;
|
|
580
|
+
};
|
|
581
|
+
return walk(root, 0, "");
|
|
582
|
+
}
|
|
583
|
+
function itemFileBase(name) {
|
|
584
|
+
const dot = name.lastIndexOf(".");
|
|
585
|
+
if (dot <= 0) return void 0;
|
|
586
|
+
const extension = name.slice(dot).toLowerCase();
|
|
587
|
+
if (extension !== ".yml" && extension !== ".yaml" && extension !== ".md") return void 0;
|
|
588
|
+
return name.slice(0, dot);
|
|
589
|
+
}
|
|
590
|
+
async function requestDoorstopGitStatus(context, request) {
|
|
591
|
+
if (request.operation !== DOORSTOP_GIT_STATUS_OPERATION) {
|
|
592
|
+
throw unsupportedBackendOperationError(request.operation);
|
|
593
|
+
}
|
|
594
|
+
parseDoorstopGitStatusRequest(request.input);
|
|
595
|
+
const settings = parseDoorstopBackendSettings(context.settings);
|
|
596
|
+
const cwd = request.workspace.path;
|
|
597
|
+
return runSerialized(context, cwd, async () => {
|
|
598
|
+
const startedAt = Date.now();
|
|
599
|
+
const inWorkTree = await baselineGit(
|
|
600
|
+
context,
|
|
601
|
+
settings,
|
|
602
|
+
cwd,
|
|
603
|
+
["rev-parse", "--is-inside-work-tree", "--show-prefix"],
|
|
604
|
+
request.signal,
|
|
605
|
+
startedAt
|
|
606
|
+
);
|
|
607
|
+
if (inWorkTree === void 0 || !isWorkTreeResult(inWorkTree)) {
|
|
608
|
+
return gitStatusResponse(false, void 0, void 0, void 0, 0, 0, []);
|
|
609
|
+
}
|
|
610
|
+
const porcelain = await baselineGit(
|
|
611
|
+
context,
|
|
612
|
+
settings,
|
|
613
|
+
cwd,
|
|
614
|
+
["status", "--porcelain=v1", "-z", "-b"],
|
|
615
|
+
request.signal,
|
|
616
|
+
startedAt
|
|
617
|
+
);
|
|
618
|
+
if (porcelain === void 0 || porcelain.exitCode !== 0) {
|
|
619
|
+
return gitStatusResponse(false, void 0, void 0, void 0, 0, 0, []);
|
|
620
|
+
}
|
|
621
|
+
const parsed = parsePorcelainV1Status(porcelain.stdout);
|
|
622
|
+
return gitStatusResponse(
|
|
623
|
+
true,
|
|
624
|
+
parsed.branch,
|
|
625
|
+
parsed.ahead,
|
|
626
|
+
parsed.behind,
|
|
627
|
+
parsed.staged,
|
|
628
|
+
parsed.dirty,
|
|
629
|
+
parsed.files.slice(0, DOORSTOP_GIT_STATUS_FILES_MAX)
|
|
630
|
+
);
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
function gitStatusResponse(git, branch, ahead, behind, staged, dirty, files) {
|
|
634
|
+
return {
|
|
635
|
+
git,
|
|
636
|
+
...branch === void 0 ? {} : { branch },
|
|
637
|
+
...ahead === void 0 ? {} : { ahead },
|
|
638
|
+
...behind === void 0 ? {} : { behind },
|
|
639
|
+
staged,
|
|
640
|
+
dirty,
|
|
641
|
+
// Each file is spread into a fresh object so the value admits the host
|
|
642
|
+
// bridge's JsonObject index signature.
|
|
643
|
+
files: files.map((file) => ({ ...file }))
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
function parsePorcelainV1Status(stdout) {
|
|
647
|
+
const output = stdout.split("\0");
|
|
648
|
+
const records = output[output.length - 1] === "" ? output.slice(0, -1) : output;
|
|
649
|
+
let branch;
|
|
650
|
+
let ahead;
|
|
651
|
+
let behind;
|
|
652
|
+
const files = [];
|
|
653
|
+
let staged = 0;
|
|
654
|
+
let dirty = 0;
|
|
655
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
656
|
+
const record = records[index];
|
|
657
|
+
if (record === void 0) continue;
|
|
658
|
+
if (index === 0 && record.startsWith("## ")) {
|
|
659
|
+
const header = parsePorcelainBranchHeader(record.slice(3));
|
|
660
|
+
branch = header.branch;
|
|
661
|
+
ahead = header.ahead;
|
|
662
|
+
behind = header.behind;
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
const entry = /^(.{2}) (.*)$/s.exec(record);
|
|
666
|
+
if (entry === null) continue;
|
|
667
|
+
const xCode = entry[1]?.[0];
|
|
668
|
+
const yCode = entry[1]?.[1];
|
|
669
|
+
const indexState = gitFileStateFromPorcelainXy(xCode);
|
|
670
|
+
const workingTreeState = gitFileStateFromPorcelainXy(yCode);
|
|
671
|
+
files.push({ path: entry[2] ?? "", index: indexState, workingTree: workingTreeState });
|
|
672
|
+
if (indexState !== "unmodified" && indexState !== "untracked" && indexState !== "ignored") {
|
|
673
|
+
staged += 1;
|
|
674
|
+
}
|
|
675
|
+
if (indexState !== "unmodified" || workingTreeState !== "unmodified") {
|
|
676
|
+
dirty += 1;
|
|
677
|
+
}
|
|
678
|
+
if (xCode === "R" || xCode === "C") index += 1;
|
|
679
|
+
}
|
|
680
|
+
return {
|
|
681
|
+
...branch === void 0 ? {} : { branch },
|
|
682
|
+
...ahead === void 0 ? {} : { ahead },
|
|
683
|
+
...behind === void 0 ? {} : { behind },
|
|
684
|
+
files,
|
|
685
|
+
staged,
|
|
686
|
+
dirty
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
function gitFileStateFromPorcelainXy(code) {
|
|
690
|
+
switch (code) {
|
|
691
|
+
case "M":
|
|
692
|
+
case "T":
|
|
693
|
+
return "modified";
|
|
694
|
+
case "A":
|
|
695
|
+
return "added";
|
|
696
|
+
case "D":
|
|
697
|
+
return "deleted";
|
|
698
|
+
case "R":
|
|
699
|
+
return "renamed";
|
|
700
|
+
case "C":
|
|
701
|
+
return "copied";
|
|
702
|
+
case "U":
|
|
703
|
+
return "conflicted";
|
|
704
|
+
case "?":
|
|
705
|
+
return "untracked";
|
|
706
|
+
case "!":
|
|
707
|
+
return "ignored";
|
|
708
|
+
default:
|
|
709
|
+
return "unmodified";
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
function parsePorcelainBranchHeader(header) {
|
|
713
|
+
let branchPart = header;
|
|
714
|
+
if (branchPart.startsWith("No commits yet on ")) {
|
|
715
|
+
branchPart = branchPart.slice("No commits yet on ".length);
|
|
716
|
+
}
|
|
717
|
+
if (branchPart === "HEAD (no branch)" || branchPart === "HEAD") return {};
|
|
718
|
+
const upstreamDot = branchPart.indexOf("...");
|
|
719
|
+
const branch = upstreamDot === -1 ? branchPart : branchPart.slice(0, upstreamDot);
|
|
720
|
+
let ahead;
|
|
721
|
+
let behind;
|
|
722
|
+
if (upstreamDot !== -1) {
|
|
723
|
+
const bracket = /^.*\[(.*)\]$/.exec(branchPart.slice(upstreamDot + 3));
|
|
724
|
+
if (bracket !== null && bracket[1] !== void 0) {
|
|
725
|
+
for (const part of bracket[1].split(",")) {
|
|
726
|
+
const aheadMatch = /^ahead (\d+)$/.exec(part.trim());
|
|
727
|
+
const behindMatch = /^behind (\d+)$/.exec(part.trim());
|
|
728
|
+
if (aheadMatch !== null) ahead = Number(aheadMatch[1]);
|
|
729
|
+
if (behindMatch !== null) behind = Number(behindMatch[1]);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
branch,
|
|
735
|
+
...ahead === void 0 ? {} : { ahead },
|
|
736
|
+
...behind === void 0 ? {} : { behind }
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
function stageAddTargets(stdout, paths) {
|
|
740
|
+
const output = stdout.split("\0");
|
|
741
|
+
const records = output[output.length - 1] === "" ? output.slice(0, -1) : output;
|
|
742
|
+
const requestSet = new Set(paths);
|
|
743
|
+
const targets = /* @__PURE__ */ new Set();
|
|
744
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
745
|
+
const record = records[index];
|
|
746
|
+
if (record === void 0) continue;
|
|
747
|
+
const entry = /^(.{2}) (.*)$/s.exec(record);
|
|
748
|
+
if (entry === null) continue;
|
|
749
|
+
const xCode = entry[1]?.[0];
|
|
750
|
+
const yCode = entry[1]?.[1];
|
|
751
|
+
if (xCode === "R" || xCode === "C") index += 1;
|
|
752
|
+
if (yCode === void 0 || yCode === " ") continue;
|
|
753
|
+
const path = entry[2] ?? "";
|
|
754
|
+
if (requestSet.has(path)) targets.add(path);
|
|
755
|
+
}
|
|
756
|
+
return [...targets];
|
|
757
|
+
}
|
|
758
|
+
function unstageResetTargets(stdout, paths) {
|
|
759
|
+
const output = stdout.split("\0");
|
|
760
|
+
const records = output[output.length - 1] === "" ? output.slice(0, -1) : output;
|
|
761
|
+
const requestSet = new Set(paths);
|
|
762
|
+
const targets = /* @__PURE__ */ new Set();
|
|
763
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
764
|
+
const record = records[index];
|
|
765
|
+
if (record === void 0) continue;
|
|
766
|
+
const entry = /^(.{2}) (.*)$/s.exec(record);
|
|
767
|
+
if (entry === null) continue;
|
|
768
|
+
const xCode = entry[1]?.[0];
|
|
769
|
+
const yCode = entry[1]?.[1];
|
|
770
|
+
if (xCode === "R" || xCode === "C") index += 1;
|
|
771
|
+
if (xCode === void 0 || xCode === " " || xCode === "?" || xCode === "!" || xCode === "U") continue;
|
|
772
|
+
if (yCode === "U" || (xCode === "A" || xCode === "D") && xCode === yCode) continue;
|
|
773
|
+
const path = entry[2] ?? "";
|
|
774
|
+
if (requestSet.has(path)) targets.add(path);
|
|
775
|
+
}
|
|
776
|
+
return [...targets];
|
|
777
|
+
}
|
|
778
|
+
async function requestDoorstopGitStage(context, request) {
|
|
779
|
+
if (request.operation !== DOORSTOP_GIT_STAGE_OPERATION) {
|
|
780
|
+
throw unsupportedBackendOperationError(request.operation);
|
|
781
|
+
}
|
|
782
|
+
const stage = parseDoorstopGitStageRequest(request.input);
|
|
783
|
+
const settings = parseDoorstopBackendSettings(context.settings);
|
|
784
|
+
const cwd = request.workspace.path;
|
|
785
|
+
return runSerialized(context, cwd, async () => {
|
|
786
|
+
const startedAt = Date.now();
|
|
787
|
+
try {
|
|
788
|
+
if (remainingBudgetMs(startedAt) <= 0) return stageResponse("skipped");
|
|
789
|
+
const inWorkTree = await runGit(
|
|
790
|
+
context,
|
|
791
|
+
settings,
|
|
792
|
+
cwd,
|
|
793
|
+
["rev-parse", "--is-inside-work-tree"],
|
|
794
|
+
request.signal,
|
|
795
|
+
gitExecTimeoutMs(settings, startedAt)
|
|
796
|
+
);
|
|
797
|
+
if (!isWorkTreeResult(inWorkTree)) return stageResponse("skipped");
|
|
798
|
+
if (remainingBudgetMs(startedAt) <= 0) return stageResponse("skipped");
|
|
799
|
+
const pathspecs = stage.paths.map(literalPathspec);
|
|
800
|
+
const status = await runGit(
|
|
801
|
+
context,
|
|
802
|
+
settings,
|
|
803
|
+
cwd,
|
|
804
|
+
["status", "--porcelain", "-z", "--", ...pathspecs],
|
|
805
|
+
request.signal,
|
|
806
|
+
gitExecTimeoutMs(settings, startedAt)
|
|
807
|
+
);
|
|
808
|
+
if (status.exitCode !== 0) {
|
|
809
|
+
gitStepFailure(status, ["status", "--porcelain", "-z", "--", ...pathspecs]);
|
|
810
|
+
}
|
|
811
|
+
const addPaths = stageAddTargets(status.stdout, stage.paths);
|
|
812
|
+
if (addPaths.length === 0) return stageResponse("clean");
|
|
813
|
+
if (remainingBudgetMs(startedAt) <= 0) return stageResponse("skipped");
|
|
814
|
+
const add = await runGit(
|
|
815
|
+
context,
|
|
816
|
+
settings,
|
|
817
|
+
cwd,
|
|
818
|
+
["add", "--", ...addPaths.map(literalPathspec)],
|
|
819
|
+
request.signal,
|
|
820
|
+
gitExecTimeoutMs(settings, startedAt)
|
|
821
|
+
);
|
|
822
|
+
if (add.exitCode !== 0) gitStepFailure(add, ["add", "--", ...addPaths.map(literalPathspec)]);
|
|
823
|
+
return stageResponse("staged", addPaths.length);
|
|
824
|
+
} catch (error) {
|
|
825
|
+
if (request.signal.aborted) throw error;
|
|
826
|
+
return stageResponse("failed", void 0, commitStderrExcerpt(formatUnknownError(error)));
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
function stageResponse(status, staged, stderr) {
|
|
831
|
+
return {
|
|
832
|
+
status,
|
|
833
|
+
...staged === void 0 ? {} : { staged },
|
|
834
|
+
...stderr === void 0 ? {} : { stderr }
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
async function requestDoorstopGitUnstage(context, request) {
|
|
838
|
+
if (request.operation !== DOORSTOP_GIT_UNSTAGE_OPERATION) {
|
|
839
|
+
throw unsupportedBackendOperationError(request.operation);
|
|
840
|
+
}
|
|
841
|
+
const unstage = parseDoorstopGitUnstageRequest(request.input);
|
|
842
|
+
const settings = parseDoorstopBackendSettings(context.settings);
|
|
843
|
+
const cwd = request.workspace.path;
|
|
844
|
+
return runSerialized(context, cwd, async () => {
|
|
845
|
+
const startedAt = Date.now();
|
|
846
|
+
try {
|
|
847
|
+
if (remainingBudgetMs(startedAt) <= 0) return unstagedResponse("skipped");
|
|
848
|
+
const inWorkTree = await runGit(
|
|
849
|
+
context,
|
|
850
|
+
settings,
|
|
851
|
+
cwd,
|
|
852
|
+
["rev-parse", "--is-inside-work-tree"],
|
|
853
|
+
request.signal,
|
|
854
|
+
gitExecTimeoutMs(settings, startedAt)
|
|
855
|
+
);
|
|
856
|
+
if (!isWorkTreeResult(inWorkTree)) return unstagedResponse("skipped");
|
|
857
|
+
if (remainingBudgetMs(startedAt) <= 0) return unstagedResponse("skipped");
|
|
858
|
+
const pathspecs = unstage.paths.map(literalPathspec);
|
|
859
|
+
const status = await runGit(
|
|
860
|
+
context,
|
|
861
|
+
settings,
|
|
862
|
+
cwd,
|
|
863
|
+
["status", "--porcelain", "-z", "--", ...pathspecs],
|
|
864
|
+
request.signal,
|
|
865
|
+
gitExecTimeoutMs(settings, startedAt)
|
|
866
|
+
);
|
|
867
|
+
if (status.exitCode !== 0) {
|
|
868
|
+
gitStepFailure(status, ["status", "--porcelain", "-z", "--", ...pathspecs]);
|
|
869
|
+
}
|
|
870
|
+
const resetPaths = unstageResetTargets(status.stdout, unstage.paths);
|
|
871
|
+
if (resetPaths.length === 0) return unstagedResponse("clean");
|
|
872
|
+
if (remainingBudgetMs(startedAt) <= 0) return unstagedResponse("skipped");
|
|
873
|
+
const reset = await runGit(
|
|
874
|
+
context,
|
|
875
|
+
settings,
|
|
876
|
+
cwd,
|
|
877
|
+
["reset", "-q", "--", ...resetPaths.map(literalPathspec)],
|
|
878
|
+
request.signal,
|
|
879
|
+
gitExecTimeoutMs(settings, startedAt)
|
|
880
|
+
);
|
|
881
|
+
if (reset.exitCode !== 0) gitStepFailure(reset, ["reset", "-q", "--", ...resetPaths.map(literalPathspec)]);
|
|
882
|
+
return unstagedResponse("unstaged", resetPaths.length);
|
|
883
|
+
} catch (error) {
|
|
884
|
+
if (request.signal.aborted) throw error;
|
|
885
|
+
return unstagedResponse("failed", void 0, commitStderrExcerpt(formatUnknownError(error)));
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
function unstagedResponse(status, unstaged, stderr) {
|
|
890
|
+
return {
|
|
891
|
+
status,
|
|
892
|
+
...unstaged === void 0 ? {} : { unstaged },
|
|
893
|
+
...stderr === void 0 ? {} : { stderr }
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
async function requestDoorstopGitCommit(context, request) {
|
|
897
|
+
if (request.operation !== DOORSTOP_GIT_COMMIT_OPERATION) {
|
|
898
|
+
throw unsupportedBackendOperationError(request.operation);
|
|
899
|
+
}
|
|
900
|
+
const commitRequest = parseDoorstopGitCommitRequest(request.input);
|
|
901
|
+
const settings = parseDoorstopBackendSettings(context.settings);
|
|
902
|
+
const cwd = request.workspace.path;
|
|
903
|
+
return runSerialized(context, cwd, async () => {
|
|
904
|
+
const startedAt = Date.now();
|
|
905
|
+
try {
|
|
906
|
+
if (remainingBudgetMs(startedAt) <= 0) return commitResponse({ status: "skipped" });
|
|
907
|
+
const inWorkTree = await runGit(
|
|
908
|
+
context,
|
|
909
|
+
settings,
|
|
910
|
+
cwd,
|
|
911
|
+
["rev-parse", "--is-inside-work-tree"],
|
|
912
|
+
request.signal,
|
|
913
|
+
gitExecTimeoutMs(settings, startedAt)
|
|
914
|
+
);
|
|
915
|
+
if (!isWorkTreeResult(inWorkTree)) {
|
|
916
|
+
return commitResponse({ status: "skipped" });
|
|
917
|
+
}
|
|
918
|
+
if (remainingBudgetMs(startedAt) <= 0) return commitResponse({ status: "skipped" });
|
|
919
|
+
const staged = await runGit(
|
|
920
|
+
context,
|
|
921
|
+
settings,
|
|
922
|
+
cwd,
|
|
923
|
+
["diff", "--cached", "--quiet"],
|
|
924
|
+
request.signal,
|
|
925
|
+
gitExecTimeoutMs(settings, startedAt)
|
|
926
|
+
);
|
|
927
|
+
if (staged.exitCode === 0) return commitResponse({ status: "clean" });
|
|
928
|
+
if (staged.exitCode !== 1) gitStepFailure(staged, ["diff", "--cached", "--quiet"]);
|
|
929
|
+
if (remainingBudgetMs(startedAt) <= 0) return commitResponse({ status: "skipped" });
|
|
930
|
+
const commit = await runGit(
|
|
931
|
+
context,
|
|
932
|
+
settings,
|
|
933
|
+
cwd,
|
|
934
|
+
["commit", "-m", commitRequest.message],
|
|
935
|
+
request.signal,
|
|
936
|
+
gitExecTimeoutMs(settings, startedAt)
|
|
937
|
+
);
|
|
938
|
+
if (commit.exitCode !== 0) gitStepFailure(commit, ["commit", "-m", commitRequest.message]);
|
|
939
|
+
if (remainingBudgetMs(startedAt) <= 0) {
|
|
940
|
+
const fallback2 = commitBracketSha(commit.stdout);
|
|
941
|
+
return fallback2 === void 0 ? commitResponse({ status: "skipped" }) : commitResponse({ status: "committed", sha: fallback2 });
|
|
942
|
+
}
|
|
943
|
+
const head = await runGit(
|
|
944
|
+
context,
|
|
945
|
+
settings,
|
|
946
|
+
cwd,
|
|
947
|
+
["rev-parse", "--short", "HEAD"],
|
|
948
|
+
request.signal,
|
|
949
|
+
gitExecTimeoutMs(settings, startedAt)
|
|
950
|
+
);
|
|
951
|
+
const fromHead = head.exitCode === 0 ? head.stdout.trim() : "";
|
|
952
|
+
if (fromHead !== "") return commitResponse({ status: "committed", sha: fromHead });
|
|
953
|
+
const fallback = commitBracketSha(commit.stdout);
|
|
954
|
+
if (fallback !== void 0) return commitResponse({ status: "committed", sha: fallback });
|
|
955
|
+
gitStepFailure(head, ["rev-parse", "--short", "HEAD"]);
|
|
956
|
+
} catch (error) {
|
|
957
|
+
if (request.signal.aborted) throw error;
|
|
958
|
+
return commitResponse({ status: "failed", stderr: commitStderrExcerpt(formatUnknownError(error)) });
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
function commitResponse(outcome) {
|
|
963
|
+
return { ...outcome };
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// src/server-plugin.ts
|
|
967
|
+
var DOORSTOP_MARKER_FILE = ".doorstop.yml";
|
|
968
|
+
var PROBE_MAX_DEPTH = 3;
|
|
969
|
+
var PROBE_MAX_ENTRIES = 256;
|
|
970
|
+
var PROBE_SKIP_NAMES = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
971
|
+
async function probeContainsDoorstopMarker(root, signal) {
|
|
972
|
+
let scanned = 0;
|
|
973
|
+
const walk = async (directory, depth) => {
|
|
974
|
+
if (scanned >= PROBE_MAX_ENTRIES) return false;
|
|
975
|
+
scanned += 1;
|
|
976
|
+
let entries;
|
|
977
|
+
try {
|
|
978
|
+
if (signal.aborted) return false;
|
|
979
|
+
entries = await readdir2(directory, { withFileTypes: true });
|
|
980
|
+
} catch {
|
|
981
|
+
return false;
|
|
982
|
+
}
|
|
983
|
+
for (const entry of entries) {
|
|
984
|
+
if (scanned >= PROBE_MAX_ENTRIES) return false;
|
|
985
|
+
scanned += 1;
|
|
986
|
+
if (entry.isFile() && entry.name === DOORSTOP_MARKER_FILE) return true;
|
|
987
|
+
if (entry.isDirectory() && depth < PROBE_MAX_DEPTH && !PROBE_SKIP_NAMES.has(entry.name)) {
|
|
988
|
+
if (await walk(join2(directory, entry.name), depth + 1)) return true;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
return false;
|
|
992
|
+
};
|
|
993
|
+
return walk(root, 0);
|
|
994
|
+
}
|
|
995
|
+
var plugin = {
|
|
996
|
+
apiVersion: 1,
|
|
997
|
+
name: "Opendoor",
|
|
998
|
+
activate(context) {
|
|
999
|
+
return { workspaceProvider: createDoorstopWorkspaceProvider(context) };
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
var server_plugin_default = plugin;
|
|
1003
|
+
function createDoorstopWorkspaceProvider(context) {
|
|
1004
|
+
return Object.freeze({
|
|
1005
|
+
async probe(project, signal) {
|
|
1006
|
+
try {
|
|
1007
|
+
if (await probeContainsDoorstopMarker(project.path, signal)) return "claim";
|
|
1008
|
+
return "pass";
|
|
1009
|
+
} catch {
|
|
1010
|
+
return "pass";
|
|
1011
|
+
}
|
|
1012
|
+
},
|
|
1013
|
+
async list(project, signal) {
|
|
1014
|
+
return [
|
|
1015
|
+
{
|
|
1016
|
+
key: project.path,
|
|
1017
|
+
path: project.path,
|
|
1018
|
+
label: project.name,
|
|
1019
|
+
isMain: true,
|
|
1020
|
+
publicMetadata: { doorstop: true }
|
|
1021
|
+
}
|
|
1022
|
+
];
|
|
1023
|
+
},
|
|
1024
|
+
request: (request) => {
|
|
1025
|
+
switch (request.operation) {
|
|
1026
|
+
case DOORSTOP_RUN_OPERATION:
|
|
1027
|
+
return requestDoorstopBackend(context, request);
|
|
1028
|
+
case DOORSTOP_BASELINE_OPERATION:
|
|
1029
|
+
return requestDoorstopBaseline(context, request);
|
|
1030
|
+
case DOORSTOP_GIT_STATUS_OPERATION:
|
|
1031
|
+
return requestDoorstopGitStatus(context, request);
|
|
1032
|
+
case DOORSTOP_GIT_STAGE_OPERATION:
|
|
1033
|
+
return requestDoorstopGitStage(context, request);
|
|
1034
|
+
case DOORSTOP_GIT_UNSTAGE_OPERATION:
|
|
1035
|
+
return requestDoorstopGitUnstage(context, request);
|
|
1036
|
+
case DOORSTOP_GIT_COMMIT_OPERATION:
|
|
1037
|
+
return requestDoorstopGitCommit(context, request);
|
|
1038
|
+
default:
|
|
1039
|
+
return Promise.reject(unsupportedBackendOperationError(request.operation));
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
export {
|
|
1045
|
+
createDoorstopWorkspaceProvider,
|
|
1046
|
+
server_plugin_default as default
|
|
1047
|
+
};
|