@agimon-ai/po-git 0.0.0-alpha.12 → 0.0.0-alpha.121
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 +149 -21
- package/README.md +16 -4
- package/dist/apiContracts.cjs +34 -1
- package/dist/apiContracts.cjs.map +1 -1
- package/dist/apiContracts.mjs +34 -1
- package/dist/apiContracts.mjs.map +1 -1
- package/dist/extensions/native.mjs +2167 -520
- package/dist/extensions/native.mjs.map +1 -1
- package/dist/extensions/server.cjs +521 -276
- package/dist/extensions/server.cjs.map +1 -1
- package/dist/extensions/server.d.mts +2 -2
- package/dist/extensions/server.d.mts.map +1 -1
- package/dist/extensions/server.mjs +518 -273
- package/dist/extensions/server.mjs.map +1 -1
- package/dist/extensions/web.mjs +1566 -1294
- package/dist/extensions/web.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.mts +5 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{worktreeOperations-DGUbUnYU.cjs → worktreeOperations-0Jrjcbg7.cjs} +289 -85
- package/dist/worktreeOperations-0Jrjcbg7.cjs.map +1 -0
- package/dist/{worktreeOperations-BSeyxUai.mjs → worktreeOperations-DA_dbDv8.mjs} +259 -85
- package/dist/worktreeOperations-DA_dbDv8.mjs.map +1 -0
- package/llms.txt +1 -1
- package/package.json +100 -20
- package/src/prompts/{po-use-git → codemode-how-to-git}/SKILL.md +35 -32
- package/dist/worktreeOperations-BSeyxUai.mjs.map +0 -1
- package/dist/worktreeOperations-DGUbUnYU.cjs.map +0 -1
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
1
|
+
import { C as READ_TIMEOUT_MS, E as runGit, S as NETWORK_TIMEOUT_MS, T as createWorktreeGit, a as GIT_WORKTREE_LIFECYCLE_EVENT, c as isWorktreeLifecycleEvent, d as registryFile, f as readJson, g as invalidRequest, h as PoGitExpectedError, i as GIT_SESSION_CHANGED_EVENT, l as credentialsFile, m as HubUnavailableError, n as createWorktreeRegistry, o as createWorktreeMessageInbox, p as writeJsonAtomic, r as validRefName, s as isSessionChangedEvent, t as createWorktreeOperations, u as poGitRoot, w as REBASE_TIMEOUT_MS } from "../worktreeOperations-DA_dbDv8.mjs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { composeServerProviderOverrides, defineServerPlugin, packageResourcePath, readPackageResource } from "@agimon-ai/po-core/serverPlugin";
|
|
6
|
+
import { composeRootHooks, composeRootInjects, defineChannel, defineHook, defineResource, defineRoot, defineRoute, defineServerTool } from "@agimon-ai/po-core/extensionFile";
|
|
7
|
+
import { PO_CODEMODE_SERVICE, readPoCodemode } from "@agimon-ai/po-core/codemode";
|
|
7
8
|
import { readPoSessionDelivery } from "@agimon-ai/po-session";
|
|
8
9
|
import { Type } from "typebox";
|
|
10
|
+
import { BACKGROUND_CONTEXT, PO_SHELL_SERVICE, requirePoShell, withAbortSignal } from "@agimon-ai/po-core/platform";
|
|
11
|
+
import { poApiCallerFrom } from "@agimon-ai/po-core/packageApi";
|
|
12
|
+
import { parseLaunchSelection } from "@agimon-ai/po-core/hubChannel";
|
|
9
13
|
//#region src/schemas/runWorktreeTool.ts
|
|
10
14
|
/**
|
|
11
15
|
* The tool's action vocabulary.
|
|
@@ -247,7 +251,8 @@ async function executeRunWorktreeTool(params, operations, context, options = {})
|
|
|
247
251
|
content: text(describe(record, context.sessionId), `Session: ${record.sessionId}`, dirtyFiles.length === 0 ? "Clean." : `${String(dirtyFiles.length)} uncommitted file(s): ${dirtyFiles.slice(0, 20).join(", ")}`),
|
|
248
252
|
details: {
|
|
249
253
|
action: params.action,
|
|
250
|
-
records: [record]
|
|
254
|
+
records: [record],
|
|
255
|
+
dirtyFiles
|
|
251
256
|
}
|
|
252
257
|
};
|
|
253
258
|
}
|
|
@@ -271,7 +276,10 @@ async function executeRunWorktreeTool(params, operations, context, options = {})
|
|
|
271
276
|
const waiting = await operations.messages(context, params.id);
|
|
272
277
|
return {
|
|
273
278
|
content: waiting.length === 0 ? text("No messages.") : text(`${String(waiting.length)} message(s):`, ...waiting.map((entry) => `[${entry.sentAt}] ${entry.from}: ${entry.text}`)),
|
|
274
|
-
details: {
|
|
279
|
+
details: {
|
|
280
|
+
action: params.action,
|
|
281
|
+
messages: waiting
|
|
282
|
+
}
|
|
275
283
|
};
|
|
276
284
|
}
|
|
277
285
|
case WORKTREE_ACTIONS.prune: {
|
|
@@ -286,12 +294,172 @@ async function executeRunWorktreeTool(params, operations, context, options = {})
|
|
|
286
294
|
`Untracked directories, left alone: ${plan.untracked.length === 0 ? "none" : plan.untracked.join(", ")}`,
|
|
287
295
|
...plan.keptBranches.length === 0 ? [] : [`Branches kept, they hold commits: ${plan.keptBranches.join(", ")}`]
|
|
288
296
|
]),
|
|
289
|
-
details: {
|
|
297
|
+
details: {
|
|
298
|
+
action: params.action,
|
|
299
|
+
plan
|
|
300
|
+
}
|
|
290
301
|
};
|
|
291
302
|
}
|
|
292
303
|
}
|
|
293
304
|
}
|
|
294
305
|
//#endregion
|
|
306
|
+
//#region src/services/worktreeNamespace/index.ts
|
|
307
|
+
const DECLARATIONS = `type WorktreeStatus = 'spawning' | 'running' | 'closing' | 'orphaned';
|
|
308
|
+
|
|
309
|
+
interface Worktree {
|
|
310
|
+
id: string;
|
|
311
|
+
branch: string;
|
|
312
|
+
baseRef: string;
|
|
313
|
+
/** Absolute path of the checkout, under ~/.po/git/worktrees. */
|
|
314
|
+
path: string;
|
|
315
|
+
repositoryRoot: string;
|
|
316
|
+
/** The session running inside the worktree. */
|
|
317
|
+
sessionId: string;
|
|
318
|
+
parentSessionId?: string;
|
|
319
|
+
status: WorktreeStatus;
|
|
320
|
+
createdAt: string;
|
|
321
|
+
/** How your session relates to it: 'parent' spawned it, 'child' runs inside it, 'other' is neither. */
|
|
322
|
+
relationship: 'parent' | 'child' | 'other';
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
interface WorktreeMessage {
|
|
326
|
+
worktreeId: string;
|
|
327
|
+
fromSessionId: string;
|
|
328
|
+
from: 'parent' | 'child';
|
|
329
|
+
text: string;
|
|
330
|
+
sentAt: string;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
interface PrunePlan {
|
|
334
|
+
remove: Worktree[];
|
|
335
|
+
forget: Worktree[];
|
|
336
|
+
/** Kept because they hold uncommitted work. */
|
|
337
|
+
keptDirty: Worktree[];
|
|
338
|
+
untracked: string[];
|
|
339
|
+
/** Branches kept because they hold commits. */
|
|
340
|
+
keptBranches: string[];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
declare const worktrees: {
|
|
344
|
+
/** Every worktree for this repository. */
|
|
345
|
+
list(): Promise<Worktree[]>;
|
|
346
|
+
/** One worktree and its uncommitted files. */
|
|
347
|
+
status(id: string): Promise<{ worktree: Worktree; dirtyFiles: string[] }>;
|
|
348
|
+
/**
|
|
349
|
+
* Create a worktree on a new branch and start a Po session in it, nested under this one. baseRef defaults to
|
|
350
|
+
* the current branch's remote-tracking ref, then the remote default branch; name labels it in the rail; task is
|
|
351
|
+
* the new session's first message. Setup can be slow, so give exec a larger timeoutMs.
|
|
352
|
+
*/
|
|
353
|
+
spawn(request: { branch: string; baseRef?: string; name?: string; task?: string }): Promise<Worktree>;
|
|
354
|
+
/** Stop the worktree's session and delete its checkout. Refused on a dirty tree unless force is true. */
|
|
355
|
+
close(id: string, options?: { force?: boolean }): Promise<Worktree>;
|
|
356
|
+
/** Merge the worktree's branch into this checkout. Refused when this checkout is dirty. */
|
|
357
|
+
merge(id: string, options?: { message?: string }): Promise<Worktree>;
|
|
358
|
+
/** Remove orphaned worktrees. With dryRun, only return the plan. */
|
|
359
|
+
prune(options?: { dryRun?: boolean }): Promise<PrunePlan>;
|
|
360
|
+
/**
|
|
361
|
+
* Message the other side of a worktree. Use the record with relationship 'child' to report to your parent,
|
|
362
|
+
* and 'parent' to reach your own children. 'other' records are not communication edges.
|
|
363
|
+
*/
|
|
364
|
+
send(id: string, message: string): Promise<void>;
|
|
365
|
+
/** Messages waiting for you from a worktree. */
|
|
366
|
+
messages(id: string): Promise<WorktreeMessage[]>;
|
|
367
|
+
};`;
|
|
368
|
+
const json = (value) => JSON.parse(JSON.stringify(value));
|
|
369
|
+
/** Git worktrees for code mode: the actions of `run_worktree` as methods, returning records instead of text. */
|
|
370
|
+
function createWorktreeNamespace(options) {
|
|
371
|
+
return {
|
|
372
|
+
name: "worktrees",
|
|
373
|
+
summary: "Git worktrees, each with its own Po session nested under this one: worktrees.list, status, spawn, close, merge, prune, send and messages.",
|
|
374
|
+
declarations: DECLARATIONS,
|
|
375
|
+
bind(run) {
|
|
376
|
+
if (run.sessionId !== options.execution.sessionId) throw new Error("Worktrees namespace belongs to another session.");
|
|
377
|
+
let disposed = false;
|
|
378
|
+
run.onDispose(() => {
|
|
379
|
+
disposed = true;
|
|
380
|
+
});
|
|
381
|
+
const view = (record) => ({
|
|
382
|
+
...record,
|
|
383
|
+
relationship: record.parentSessionId === options.execution.sessionId ? "parent" : record.sessionId === options.execution.sessionId ? "child" : "other"
|
|
384
|
+
});
|
|
385
|
+
/** One `run_worktree` action through the tool's own code, so the tool and the namespace cannot drift. */
|
|
386
|
+
const act = async (name, action, input) => {
|
|
387
|
+
run.signal.throwIfAborted();
|
|
388
|
+
if (disposed) throw new Error("Worktrees namespace run is disposed.");
|
|
389
|
+
options.assertAvailable();
|
|
390
|
+
const params = validateParams({
|
|
391
|
+
action,
|
|
392
|
+
...typeof input === "object" && input !== null ? input : {}
|
|
393
|
+
});
|
|
394
|
+
const context = {
|
|
395
|
+
cwd: options.execution.cwd,
|
|
396
|
+
sessionId: options.execution.sessionId
|
|
397
|
+
};
|
|
398
|
+
const started = Date.now();
|
|
399
|
+
try {
|
|
400
|
+
const result = await executeRunWorktreeTool(params, options.operations, context, { signal: run.signal });
|
|
401
|
+
options.assertAvailable();
|
|
402
|
+
run.record?.({
|
|
403
|
+
name: `worktrees.${name}`,
|
|
404
|
+
tool: RUN_WORKTREE_TOOL_NAME,
|
|
405
|
+
args: params,
|
|
406
|
+
ok: true,
|
|
407
|
+
ms: Date.now() - started,
|
|
408
|
+
result
|
|
409
|
+
});
|
|
410
|
+
return result;
|
|
411
|
+
} catch (error) {
|
|
412
|
+
run.record?.({
|
|
413
|
+
name: `worktrees.${name}`,
|
|
414
|
+
tool: RUN_WORKTREE_TOOL_NAME,
|
|
415
|
+
args: params,
|
|
416
|
+
ok: false,
|
|
417
|
+
ms: Date.now() - started,
|
|
418
|
+
error: error instanceof Error ? error.message : String(error)
|
|
419
|
+
});
|
|
420
|
+
throw error;
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
const record = (result) => view(result.details.records[0]);
|
|
424
|
+
const members = Object.create(null);
|
|
425
|
+
members.list = async () => json((await act("list", "list", {})).details.records.map(view));
|
|
426
|
+
members.status = async (id) => {
|
|
427
|
+
const { details } = await act("status", "status", { id });
|
|
428
|
+
return json({
|
|
429
|
+
worktree: view(details.records[0]),
|
|
430
|
+
dirtyFiles: details.dirtyFiles ?? []
|
|
431
|
+
});
|
|
432
|
+
};
|
|
433
|
+
members.spawn = async (request) => json(record(await act("spawn", "spawn_worktree", request)));
|
|
434
|
+
members.close = async (id, settings) => json(record(await act("close", "close_worktree", {
|
|
435
|
+
...settings,
|
|
436
|
+
id
|
|
437
|
+
})));
|
|
438
|
+
members.merge = async (id, settings) => json(record(await act("merge", "merge", {
|
|
439
|
+
...settings,
|
|
440
|
+
id
|
|
441
|
+
})));
|
|
442
|
+
members.prune = async (settings) => {
|
|
443
|
+
const { plan } = (await act("prune", "prune", settings)).details;
|
|
444
|
+
return json({
|
|
445
|
+
...plan,
|
|
446
|
+
remove: plan.remove.map(view),
|
|
447
|
+
forget: plan.forget.map(view),
|
|
448
|
+
keptDirty: plan.keptDirty.map(view)
|
|
449
|
+
});
|
|
450
|
+
};
|
|
451
|
+
members.send = async (id, message) => {
|
|
452
|
+
await act("send", "send", {
|
|
453
|
+
id,
|
|
454
|
+
message
|
|
455
|
+
});
|
|
456
|
+
};
|
|
457
|
+
members.messages = async (id) => json((await act("messages", "messages", { id })).details.messages);
|
|
458
|
+
return members;
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
//#endregion
|
|
295
463
|
//#region src/extensions/workspaces/sessions/(backend)/_lib/root.server.ts
|
|
296
464
|
function progressResult(action, label) {
|
|
297
465
|
return {
|
|
@@ -302,9 +470,9 @@ function progressResult(action, label) {
|
|
|
302
470
|
details: { action }
|
|
303
471
|
};
|
|
304
472
|
}
|
|
305
|
-
function createGitSession({ context, host: serverHost }) {
|
|
306
|
-
if (serverHost.context.directEvents === void 0) throw new Error("Git
|
|
307
|
-
if (serverHost.context.sessionId === void 0) throw new Error("Git
|
|
473
|
+
function createGitSession({ context, host: serverHost, agent, signal }) {
|
|
474
|
+
if (serverHost.context.directEvents === void 0) throw new Error("Git server plugin requires the session direct event bus.");
|
|
475
|
+
if (serverHost.context.sessionId === void 0) throw new Error("Git server plugin requires a session identity.");
|
|
308
476
|
const sessionId = serverHost.context.sessionId;
|
|
309
477
|
const directEvents = serverHost.context.directEvents;
|
|
310
478
|
const messageInbox = createWorktreeMessageInbox(directEvents, sessionId);
|
|
@@ -315,20 +483,12 @@ function createGitSession({ context, host: serverHost }) {
|
|
|
315
483
|
sessionDelivery: () => readPoSessionDelivery(context)
|
|
316
484
|
});
|
|
317
485
|
const resources = [{
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
attribution: {
|
|
321
|
-
kind: "minor",
|
|
322
|
-
mode: "help"
|
|
323
|
-
}
|
|
324
|
-
},
|
|
325
|
-
name: "po-git",
|
|
326
|
-
kind: "context",
|
|
327
|
-
read: () => readPackageResource(import.meta.url, "llms.txt")
|
|
328
|
-
}, {
|
|
329
|
-
name: "po-use-git",
|
|
486
|
+
name: "codemode-how-to-git",
|
|
487
|
+
description: "Hand a branch of work to a separate session in its own checkout, keeping the current checkout untouched. Use for a long refactor, a risky migration, or parallel features that touch the same files; not for parallel work on one branch.",
|
|
330
488
|
kind: "skill",
|
|
331
|
-
|
|
489
|
+
pinned: true,
|
|
490
|
+
path: packageResourcePath(import.meta.url, "src/prompts/codemode-how-to-git/SKILL.md"),
|
|
491
|
+
read: () => readPackageResource(import.meta.url, "src/prompts/codemode-how-to-git/SKILL.md")
|
|
332
492
|
}];
|
|
333
493
|
const tool = {
|
|
334
494
|
name: RUN_WORKTREE_TOOL_NAME,
|
|
@@ -348,17 +508,37 @@ function createGitSession({ context, host: serverHost }) {
|
|
|
348
508
|
const changed = (_event, execution) => {
|
|
349
509
|
directEvents.publish(GIT_SESSION_CHANGED_EVENT, execution.sessionId, { version: 1 });
|
|
350
510
|
};
|
|
511
|
+
const hooks = [{
|
|
512
|
+
event: "tool_execution_end",
|
|
513
|
+
handle: changed
|
|
514
|
+
}, {
|
|
515
|
+
event: "agent_settled",
|
|
516
|
+
handle: changed
|
|
517
|
+
}];
|
|
518
|
+
if (agent) {
|
|
519
|
+
const namespace = createWorktreeNamespace({
|
|
520
|
+
operations,
|
|
521
|
+
execution: agent.context,
|
|
522
|
+
assertAvailable: () => {
|
|
523
|
+
if (signal?.aborted) throw new Error("Git session is unavailable.");
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
context.inject([PO_CODEMODE_SERVICE], (scoped) => {
|
|
527
|
+
const codemode = readPoCodemode(scoped);
|
|
528
|
+
if (!codemode) return void 0;
|
|
529
|
+
const registration = codemode.register("@agimon-ai/po-git", namespace);
|
|
530
|
+
const restriction = agent.registerToolRestriction({ excludedTools: [RUN_WORKTREE_TOOL_NAME] });
|
|
531
|
+
return () => {
|
|
532
|
+
restriction.dispose();
|
|
533
|
+
registration.dispose();
|
|
534
|
+
};
|
|
535
|
+
});
|
|
536
|
+
}
|
|
351
537
|
return {
|
|
352
538
|
value: {
|
|
353
539
|
resources,
|
|
354
540
|
tool,
|
|
355
|
-
hooks
|
|
356
|
-
event: "tool_execution_end",
|
|
357
|
-
handle: changed
|
|
358
|
-
}, {
|
|
359
|
-
event: "agent_settled",
|
|
360
|
-
handle: changed
|
|
361
|
-
}]
|
|
541
|
+
hooks
|
|
362
542
|
},
|
|
363
543
|
onDispose: () => messageInbox.close()
|
|
364
544
|
};
|
|
@@ -469,11 +649,11 @@ const NAME_STATUS = {
|
|
|
469
649
|
T: "modified"
|
|
470
650
|
};
|
|
471
651
|
/** Line count of a small regular file, or undefined when it is binary, large, or not a regular file. */
|
|
472
|
-
function untrackedLines(root, relPath) {
|
|
652
|
+
async function untrackedLines(root, relPath) {
|
|
473
653
|
const absolute = path.join(root, relPath);
|
|
474
654
|
let stat;
|
|
475
655
|
try {
|
|
476
|
-
stat = fs.
|
|
656
|
+
stat = await fs.lstat(absolute);
|
|
477
657
|
} catch {
|
|
478
658
|
return;
|
|
479
659
|
}
|
|
@@ -482,7 +662,7 @@ function untrackedLines(root, relPath) {
|
|
|
482
662
|
binary: false
|
|
483
663
|
};
|
|
484
664
|
if (!stat.isFile() || stat.size > MAX_FILE_BYTES) return void 0;
|
|
485
|
-
const content = fs.
|
|
665
|
+
const content = await fs.readFile(absolute);
|
|
486
666
|
if (content.subarray(0, BINARY_PROBE_BYTES).includes(0)) return {
|
|
487
667
|
lines: 0,
|
|
488
668
|
binary: true
|
|
@@ -605,8 +785,8 @@ function createBranchDiff() {
|
|
|
605
785
|
"--exclude-standard",
|
|
606
786
|
"-z"
|
|
607
787
|
]));
|
|
608
|
-
untracked.
|
|
609
|
-
const counted = index < MAX_UNTRACKED_COUNTED ? untrackedLines(root, relPath) : void 0;
|
|
788
|
+
for (const [index, relPath] of untracked.entries()) {
|
|
789
|
+
const counted = index < MAX_UNTRACKED_COUNTED ? await untrackedLines(root, relPath) : void 0;
|
|
610
790
|
entries.push({
|
|
611
791
|
path: relPath,
|
|
612
792
|
status: "untracked",
|
|
@@ -614,18 +794,20 @@ function createBranchDiff() {
|
|
|
614
794
|
removed: 0,
|
|
615
795
|
...counted?.binary ? { binary: true } : {}
|
|
616
796
|
});
|
|
617
|
-
}
|
|
797
|
+
}
|
|
618
798
|
entries.sort((left, right) => left.path.localeCompare(right.path));
|
|
619
799
|
const conflicts = [...unmerged];
|
|
620
|
-
const
|
|
800
|
+
const rebaseMerge = await readLine$1(root, [
|
|
621
801
|
"rev-parse",
|
|
622
802
|
"--git-path",
|
|
623
803
|
"rebase-merge"
|
|
624
|
-
])
|
|
804
|
+
]);
|
|
805
|
+
const rebaseApply = await readLine$1(root, [
|
|
625
806
|
"rev-parse",
|
|
626
807
|
"--git-path",
|
|
627
808
|
"rebase-apply"
|
|
628
|
-
])
|
|
809
|
+
]);
|
|
810
|
+
const rebasing = (await Promise.all([rebaseMerge, rebaseApply].map(async (gitPath) => gitPath === void 0 ? false : fs.access(path.resolve(root, gitPath)).then(() => true, () => false)))).some(Boolean);
|
|
629
811
|
const upstreamRef = branch === void 0 ? void 0 : await upstreamOf(root, branch);
|
|
630
812
|
let upstream;
|
|
631
813
|
if (upstreamRef !== void 0) {
|
|
@@ -670,7 +852,7 @@ function createBranchDiff() {
|
|
|
670
852
|
if (entry === void 0) throw new Error("That path is not in this review.");
|
|
671
853
|
if (entry.status === "untracked") {
|
|
672
854
|
const absolute = path.join(review.root, entry.path);
|
|
673
|
-
const stat = fs.
|
|
855
|
+
const stat = await fs.lstat(absolute);
|
|
674
856
|
if (stat.isSymbolicLink()) return {
|
|
675
857
|
path: entry.path,
|
|
676
858
|
hunks: [{
|
|
@@ -678,7 +860,7 @@ function createBranchDiff() {
|
|
|
678
860
|
rows: [{
|
|
679
861
|
marker: "+",
|
|
680
862
|
line: 1,
|
|
681
|
-
content: fs.
|
|
863
|
+
content: await fs.readlink(absolute)
|
|
682
864
|
}]
|
|
683
865
|
}]
|
|
684
866
|
};
|
|
@@ -687,7 +869,7 @@ function createBranchDiff() {
|
|
|
687
869
|
hunks: [],
|
|
688
870
|
tooLarge: true
|
|
689
871
|
};
|
|
690
|
-
const content = fs.
|
|
872
|
+
const content = await fs.readFile(absolute);
|
|
691
873
|
if (content.subarray(0, BINARY_PROBE_BYTES).includes(0)) return {
|
|
692
874
|
path: entry.path,
|
|
693
875
|
hunks: [],
|
|
@@ -804,9 +986,9 @@ function isRecord(value) {
|
|
|
804
986
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
805
987
|
}
|
|
806
988
|
/** The workspace key: the canonical root, so a symlinked path and its target share one entry. */
|
|
807
|
-
function workspaceKey(workspaceRoot) {
|
|
989
|
+
async function workspaceKey(workspaceRoot) {
|
|
808
990
|
try {
|
|
809
|
-
return fs.
|
|
991
|
+
return await fs.realpath(workspaceRoot);
|
|
810
992
|
} catch {
|
|
811
993
|
return path.resolve(workspaceRoot);
|
|
812
994
|
}
|
|
@@ -855,7 +1037,7 @@ function publicView(config) {
|
|
|
855
1037
|
* the saved one, and only for the same host: a token never follows a host it
|
|
856
1038
|
* was not entered for.
|
|
857
1039
|
*/
|
|
858
|
-
function parseGitAuthSave(request, current, homeDir) {
|
|
1040
|
+
async function parseGitAuthSave(request, current, homeDir) {
|
|
859
1041
|
if (!isRecord(request)) throw invalidRequest("The git auth request is not an object.", "Send method none, ssh or https.");
|
|
860
1042
|
if (request.method === "none") return { method: "none" };
|
|
861
1043
|
if (request.method === "ssh") {
|
|
@@ -865,7 +1047,7 @@ function parseGitAuthSave(request, current, homeDir) {
|
|
|
865
1047
|
if (!keyPath.startsWith("/") && !keyPath.startsWith("~/")) throw invalidRequest("The key path is not absolute.", "Give the key as an absolute path or one under ~/.");
|
|
866
1048
|
let isFile = false;
|
|
867
1049
|
try {
|
|
868
|
-
isFile = fs.
|
|
1050
|
+
isFile = (await fs.stat(expandHome(keyPath, homeDir))).isFile();
|
|
869
1051
|
} catch {
|
|
870
1052
|
isFile = false;
|
|
871
1053
|
}
|
|
@@ -900,16 +1082,16 @@ function parseGitAuthSave(request, current, homeDir) {
|
|
|
900
1082
|
throw invalidRequest("The git auth method is unknown.", "Send method none, ssh or https.");
|
|
901
1083
|
}
|
|
902
1084
|
/** Tightens a file or folder something left readable by group or others. */
|
|
903
|
-
function tighten(target, mode) {
|
|
1085
|
+
async function tighten(target, mode) {
|
|
904
1086
|
try {
|
|
905
|
-
if ((fs.
|
|
1087
|
+
if (((await fs.stat(target)).mode & LOOSE_BITS) !== 0) await fs.chmod(target, mode);
|
|
906
1088
|
} catch {}
|
|
907
1089
|
}
|
|
908
1090
|
function createGitAuthStore(homeDir = os.homedir()) {
|
|
909
1091
|
const file = credentialsFile(homeDir);
|
|
910
|
-
const readFile = () => {
|
|
911
|
-
tighten(file, OWNER_ONLY_FILE);
|
|
912
|
-
const raw = readJson(file);
|
|
1092
|
+
const readFile = async () => {
|
|
1093
|
+
await tighten(file, OWNER_ONLY_FILE);
|
|
1094
|
+
const raw = await readJson(file);
|
|
913
1095
|
const workspaces = {};
|
|
914
1096
|
if (isRecord(raw) && raw.version === 1 && isRecord(raw.workspaces)) for (const [key, value] of Object.entries(raw.workspaces)) {
|
|
915
1097
|
const config = parseConfig(value);
|
|
@@ -920,28 +1102,28 @@ function createGitAuthStore(homeDir = os.homedir()) {
|
|
|
920
1102
|
workspaces
|
|
921
1103
|
};
|
|
922
1104
|
};
|
|
923
|
-
const read = (workspaceRoot) => readFile().workspaces[workspaceKey(workspaceRoot)] ?? { method: "none" };
|
|
1105
|
+
const read = async (workspaceRoot) => (await readFile()).workspaces[await workspaceKey(workspaceRoot)] ?? { method: "none" };
|
|
924
1106
|
return {
|
|
925
1107
|
read,
|
|
926
|
-
view: (workspaceRoot) => publicView(read(workspaceRoot)),
|
|
927
|
-
save(workspaceRoot, request) {
|
|
928
|
-
const key = workspaceKey(workspaceRoot);
|
|
929
|
-
const current = readFile();
|
|
930
|
-
const next = parseGitAuthSave(request, current.workspaces[key] ?? { method: "none" }, homeDir);
|
|
1108
|
+
view: async (workspaceRoot) => publicView(await read(workspaceRoot)),
|
|
1109
|
+
async save(workspaceRoot, request) {
|
|
1110
|
+
const key = await workspaceKey(workspaceRoot);
|
|
1111
|
+
const current = await readFile();
|
|
1112
|
+
const next = await parseGitAuthSave(request, current.workspaces[key] ?? { method: "none" }, homeDir);
|
|
931
1113
|
const root = poGitRoot(homeDir);
|
|
932
|
-
fs.
|
|
1114
|
+
await fs.mkdir(root, {
|
|
933
1115
|
recursive: true,
|
|
934
1116
|
mode: OWNER_ONLY_DIRECTORY
|
|
935
1117
|
});
|
|
936
|
-
fs.
|
|
1118
|
+
await fs.chmod(root, OWNER_ONLY_DIRECTORY);
|
|
937
1119
|
const workspaces = { ...current.workspaces };
|
|
938
1120
|
if (next.method === "none") delete workspaces[key];
|
|
939
1121
|
else workspaces[key] = next;
|
|
940
|
-
writeJsonAtomic(file, {
|
|
1122
|
+
await writeJsonAtomic(file, {
|
|
941
1123
|
version: 1,
|
|
942
1124
|
workspaces
|
|
943
1125
|
});
|
|
944
|
-
fs.
|
|
1126
|
+
await fs.chmod(file, OWNER_ONLY_FILE);
|
|
945
1127
|
return publicView(next);
|
|
946
1128
|
}
|
|
947
1129
|
};
|
|
@@ -1060,7 +1242,13 @@ function newSessionRequest(body) {
|
|
|
1060
1242
|
if (branch === void 0 || branch === false || !validRefName(branch)) return { error: "A valid branch name is required." };
|
|
1061
1243
|
const name = optionalText(record.name);
|
|
1062
1244
|
if (name === false || name !== void 0 && name.length > MAX_NAME_LENGTH) return { error: `The session name must be text of up to ${String(MAX_NAME_LENGTH)} characters.` };
|
|
1063
|
-
const
|
|
1245
|
+
const chosen = record.selection === void 0 ? {} : parseLaunchSelection(record.selection);
|
|
1246
|
+
if ("error" in chosen) return chosen;
|
|
1247
|
+
const profile = chosen.selection?.profile;
|
|
1248
|
+
const named = {
|
|
1249
|
+
...name === void 0 ? {} : { name },
|
|
1250
|
+
...profile === void 0 ? {} : { selection: { profile } }
|
|
1251
|
+
};
|
|
1064
1252
|
if (record.mode === "existing-branch") {
|
|
1065
1253
|
const remote = optionalText(record.remote);
|
|
1066
1254
|
if (remote === false || remote !== void 0 && !validRefName(remote)) return { error: "That remote name is not valid." };
|
|
@@ -1106,177 +1294,237 @@ function failed(error) {
|
|
|
1106
1294
|
}, { status: error.retryable ? 503 : 409 });
|
|
1107
1295
|
return Response.json({ error: "The worktree operation failed." }, { status: 500 });
|
|
1108
1296
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1297
|
+
function createGitApi(openDirectory) {
|
|
1298
|
+
return {
|
|
1299
|
+
basePath: "git",
|
|
1300
|
+
start(context) {
|
|
1301
|
+
if (context.directEvents === void 0) throw new Error("Git hub API requires the direct event bus.");
|
|
1302
|
+
const directEvents = context.directEvents;
|
|
1303
|
+
const publishLifecycle = (sessionId, repositoryRoot) => {
|
|
1304
|
+
directEvents.publish(GIT_WORKTREE_LIFECYCLE_EVENT, sessionId, {
|
|
1305
|
+
version: 1,
|
|
1306
|
+
repositoryRoot
|
|
1307
|
+
});
|
|
1308
|
+
};
|
|
1309
|
+
const git = createWorktreeGit();
|
|
1310
|
+
const operations = createWorktreeOperations({
|
|
1311
|
+
git,
|
|
1312
|
+
sessionService: context.sessionService,
|
|
1313
|
+
...context.homeDirectory === void 0 ? {} : { homeDir: context.homeDirectory }
|
|
1120
1314
|
});
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1315
|
+
/**
|
|
1316
|
+
* The new-session dialog's routes: the workspace's branches, and a top-level
|
|
1317
|
+
* worktree session on one. Workspace-mounted only, and always rooted at the
|
|
1318
|
+
* mount's own workspace, so a request can never name another directory.
|
|
1319
|
+
*/
|
|
1320
|
+
const newSession = async (request, url) => {
|
|
1321
|
+
const isBranches = request.method === "GET" && url.pathname === "/branches";
|
|
1322
|
+
const isCreate = request.method === "POST" && url.pathname === "/sessions";
|
|
1323
|
+
const isDelete = request.method === "DELETE" && url.pathname === "/branches";
|
|
1324
|
+
const isOpen = request.method === "POST" && url.pathname === `/branches/open`;
|
|
1325
|
+
if (!isBranches && !isCreate && !isDelete && !isOpen) return void 0;
|
|
1326
|
+
const workspaceRoot = context.scope === "workspace" ? context.workspaceRoot : void 0;
|
|
1327
|
+
if (workspaceRoot === void 0) return Response.json({ error: "This route belongs to a workspace." }, { status: 404 });
|
|
1328
|
+
if (isBranches) {
|
|
1329
|
+
const repositoryRoot = await git.repositoryRoot(workspaceRoot);
|
|
1330
|
+
const branches = repositoryRoot === void 0 ? {
|
|
1331
|
+
repository: false,
|
|
1332
|
+
local: [],
|
|
1333
|
+
remote: []
|
|
1334
|
+
} : {
|
|
1335
|
+
repository: true,
|
|
1336
|
+
...await git.listBranches(repositoryRoot)
|
|
1337
|
+
};
|
|
1338
|
+
return Response.json(branches);
|
|
1339
|
+
}
|
|
1340
|
+
if (isDelete || isOpen) {
|
|
1341
|
+
if (isOpen && poApiCallerFrom(request.headers)?.locality !== "local") return Response.json({ error: "Opening Finder is only available on the host." }, { status: 403 });
|
|
1342
|
+
const body = await request.json().catch(() => void 0);
|
|
1343
|
+
const branch = typeof body === "object" && body !== null ? body.branch : void 0;
|
|
1344
|
+
if (typeof branch !== "string" || !validRefName(branch)) return badRequest("A valid branch name is required.");
|
|
1345
|
+
const repositoryRoot = await git.repositoryRoot(workspaceRoot);
|
|
1346
|
+
if (repositoryRoot === void 0) return badRequest("This workspace is not a git checkout.");
|
|
1347
|
+
const branches = await git.listBranches(repositoryRoot);
|
|
1348
|
+
const local = branches.local.find((item) => item.name === branch);
|
|
1349
|
+
if (local === void 0) return Response.json({ error: "That local branch no longer exists." }, { status: 404 });
|
|
1350
|
+
if (isOpen) {
|
|
1351
|
+
if (local.checkedOutAt === void 0) return Response.json({ error: "This branch has no checked-out folder." }, { status: 409 });
|
|
1352
|
+
if (openDirectory === void 0) return Response.json({ error: "Finder is not available on this host." }, { status: 409 });
|
|
1353
|
+
try {
|
|
1354
|
+
await openDirectory(local.checkedOutAt);
|
|
1355
|
+
return Response.json({ opened: true });
|
|
1356
|
+
} catch {
|
|
1357
|
+
return Response.json({ error: "The branch folder could not be opened in Finder." }, { status: 409 });
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
if (local.checkedOutAt !== void 0 || branches.current === branch) return Response.json({ error: "Close this branch’s worktree before deleting it." }, { status: 409 });
|
|
1361
|
+
if (!await git.deleteBranch({
|
|
1362
|
+
repositoryRoot,
|
|
1363
|
+
branch
|
|
1364
|
+
})) return Response.json({ error: "Git refused to delete this branch. It may contain unmerged commits or be checked out." }, { status: 409 });
|
|
1365
|
+
return Response.json({ deleted: true });
|
|
1366
|
+
}
|
|
1367
|
+
const parsed = newSessionRequest(await request.json().catch(() => void 0));
|
|
1368
|
+
if ("error" in parsed) return badRequest(parsed.error);
|
|
1369
|
+
const record = await operations.spawn({ cwd: workspaceRoot }, {
|
|
1370
|
+
branch: parsed.branch,
|
|
1371
|
+
checkout: parsed.mode === "existing-branch" ? "existing" : "new",
|
|
1372
|
+
...parsed.mode === "existing-branch" && parsed.remote !== void 0 ? { remote: parsed.remote } : {},
|
|
1373
|
+
...parsed.mode === "new-branch" && parsed.baseRef !== void 0 ? { baseRef: parsed.baseRef } : {},
|
|
1374
|
+
...parsed.name === void 0 ? {} : { name: parsed.name },
|
|
1375
|
+
...parsed.selection === void 0 ? {} : { selection: parsed.selection }
|
|
1376
|
+
});
|
|
1377
|
+
publishLifecycle(record.sessionId, record.repositoryRoot);
|
|
1378
|
+
const created = {
|
|
1379
|
+
sessionId: record.sessionId,
|
|
1380
|
+
worktreeId: record.id
|
|
1148
1381
|
};
|
|
1149
|
-
return Response.json(
|
|
1150
|
-
}
|
|
1151
|
-
const parsed = newSessionRequest(await request.json().catch(() => void 0));
|
|
1152
|
-
if ("error" in parsed) return badRequest(parsed.error);
|
|
1153
|
-
const record = await operations.spawn({ cwd: workspaceRoot }, {
|
|
1154
|
-
branch: parsed.branch,
|
|
1155
|
-
checkout: parsed.mode === "existing-branch" ? "existing" : "new",
|
|
1156
|
-
...parsed.mode === "existing-branch" && parsed.remote !== void 0 ? { remote: parsed.remote } : {},
|
|
1157
|
-
...parsed.mode === "new-branch" && parsed.baseRef !== void 0 ? { baseRef: parsed.baseRef } : {},
|
|
1158
|
-
...parsed.name === void 0 ? {} : { name: parsed.name }
|
|
1159
|
-
});
|
|
1160
|
-
publishLifecycle(record.sessionId, record.repositoryRoot);
|
|
1161
|
-
const created = {
|
|
1162
|
-
sessionId: record.sessionId,
|
|
1163
|
-
worktreeId: record.id
|
|
1382
|
+
return Response.json(created, { status: 201 });
|
|
1164
1383
|
};
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
if (request.method === "GET") return Response.json(gitAuth.view(root));
|
|
1191
|
-
try {
|
|
1192
|
-
return Response.json(gitAuth.save(root, await request.json().catch(() => void 0)));
|
|
1193
|
-
} catch (error) {
|
|
1194
|
-
if (error instanceof PoGitExpectedError && error.code === "invalid_request") {
|
|
1195
|
-
const text = error.message.replace(/^\[[a-z_]+\] /u, "").replace("\nRecovery: ", " ");
|
|
1196
|
-
return Response.json({
|
|
1197
|
-
error: text,
|
|
1198
|
-
code: error.code
|
|
1199
|
-
}, { status: 400 });
|
|
1200
|
-
}
|
|
1201
|
-
throw error;
|
|
1202
|
-
}
|
|
1203
|
-
};
|
|
1204
|
-
/**
|
|
1205
|
-
* The session's change against its base. Session mounts only, rooted at the
|
|
1206
|
-
* session's own checkout; a file is served only when it is in the change set
|
|
1207
|
-
* computed for the request. Overlapping requests share an in-flight snapshot.
|
|
1208
|
-
*/
|
|
1209
|
-
const review = async (request, url) => {
|
|
1210
|
-
if (url.pathname !== "/review" && url.pathname !== "/review/file") return void 0;
|
|
1211
|
-
const cwd = context.scope === "session" ? context.cwd : void 0;
|
|
1212
|
-
if (cwd === void 0) return Response.json({ error: "This route belongs to a session." }, { status: 404 });
|
|
1213
|
-
if (request.method !== "GET") return Response.json({ error: "Use GET." }, { status: 405 });
|
|
1214
|
-
let recordedBaseRef;
|
|
1215
|
-
try {
|
|
1216
|
-
recordedBaseRef = createWorktreeRegistry(registryFile(cwd, homeDir)).list().find((record) => record.sessionId === context.sessionId)?.baseRef;
|
|
1217
|
-
} catch {
|
|
1218
|
-
recordedBaseRef = void 0;
|
|
1219
|
-
}
|
|
1220
|
-
const key = JSON.stringify([cwd, recordedBaseRef ?? null]);
|
|
1221
|
-
let pending = pendingReviews.get(key);
|
|
1222
|
-
if (pending === void 0) {
|
|
1223
|
-
pending = branchDiff.review(cwd, recordedBaseRef === void 0 ? {} : { recordedBaseRef }).finally(() => pendingReviews.delete(key));
|
|
1224
|
-
pendingReviews.set(key, pending);
|
|
1225
|
-
}
|
|
1226
|
-
const result = await pending;
|
|
1227
|
-
if (url.pathname === "/review") return Response.json(result?.summary ?? {
|
|
1228
|
-
repository: false,
|
|
1229
|
-
files: []
|
|
1230
|
-
});
|
|
1231
|
-
const filePath = url.searchParams.get("path") ?? "";
|
|
1232
|
-
if (result === void 0 || !result.summary.files.some((file) => file.path === filePath)) return Response.json({ error: "That file is not part of this review." }, { status: 404 });
|
|
1233
|
-
return Response.json(await branchDiff.fileDiff(result, filePath));
|
|
1234
|
-
};
|
|
1235
|
-
return {
|
|
1236
|
-
async fetch(request) {
|
|
1237
|
-
const url = new URL(request.url);
|
|
1384
|
+
/** The admitted root for a request, or a response explaining why not. */
|
|
1385
|
+
const rootOf = (url) => {
|
|
1386
|
+
const repositoryId = url.searchParams.get("repositoryId");
|
|
1387
|
+
if (repositoryId === null || repositoryId === "") return badRequest("A repositoryId is required.");
|
|
1388
|
+
const root = context.resolveRepository?.(repositoryId);
|
|
1389
|
+
if (root === void 0) return badRequest("That repository is not admitted to this cockpit.");
|
|
1390
|
+
return root;
|
|
1391
|
+
};
|
|
1392
|
+
const homeDir = context.homeDirectory ?? context.environment?.HOME;
|
|
1393
|
+
const gitAuth = homeDir === void 0 ? void 0 : createGitAuthStore(homeDir);
|
|
1394
|
+
const branchDiff = createBranchDiff();
|
|
1395
|
+
const pendingReviews = /* @__PURE__ */ new Map();
|
|
1396
|
+
/**
|
|
1397
|
+
* A workspace's remote auth. Workspace mounts only, so the step-up gate on
|
|
1398
|
+
* `PUT /api/workspaces/<id>/plugins/git/auth` is the one way to write it.
|
|
1399
|
+
* The view never carries the token.
|
|
1400
|
+
*/
|
|
1401
|
+
const auth = async (request, url) => {
|
|
1402
|
+
if (url.pathname !== "/auth") return void 0;
|
|
1403
|
+
if (context.scope !== "workspace") return Response.json({ error: "This route belongs to a workspace." }, { status: 404 });
|
|
1404
|
+
if (request.method !== "GET" && request.method !== "PUT") return Response.json({ error: "Use GET or PUT." }, { status: 405 });
|
|
1405
|
+
if (gitAuth === void 0) throw new Error("Git auth requires the admitted host home directory.");
|
|
1406
|
+
const root = rootOf(url);
|
|
1407
|
+
if (root instanceof Response) return root;
|
|
1408
|
+
if (request.method === "GET") return Response.json(await gitAuth.view(root));
|
|
1238
1409
|
try {
|
|
1239
|
-
|
|
1240
|
-
if (answered !== void 0) return answered;
|
|
1410
|
+
return Response.json(await gitAuth.save(root, await request.json().catch(() => void 0)));
|
|
1241
1411
|
} catch (error) {
|
|
1242
|
-
|
|
1243
|
-
|
|
1412
|
+
if (error instanceof PoGitExpectedError && error.code === "invalid_request") {
|
|
1413
|
+
const text = error.message.replace(/^\[[a-z_]+\] /u, "").replace("\nRecovery: ", " ");
|
|
1414
|
+
return Response.json({
|
|
1415
|
+
error: text,
|
|
1416
|
+
code: error.code
|
|
1417
|
+
}, { status: 400 });
|
|
1418
|
+
}
|
|
1419
|
+
throw error;
|
|
1244
1420
|
}
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1421
|
+
};
|
|
1422
|
+
/**
|
|
1423
|
+
* The session's change against its base. Session mounts only, rooted at the
|
|
1424
|
+
* session's own checkout; a file is served only when it is in the change set
|
|
1425
|
+
* computed for the request. Overlapping requests share an in-flight snapshot.
|
|
1426
|
+
*/
|
|
1427
|
+
const review = async (request, url) => {
|
|
1428
|
+
if (url.pathname !== "/review" && url.pathname !== "/review/file") return void 0;
|
|
1429
|
+
const cwd = context.scope === "session" ? context.cwd : void 0;
|
|
1430
|
+
if (cwd === void 0) return Response.json({ error: "This route belongs to a session." }, { status: 404 });
|
|
1431
|
+
if (request.method !== "GET") return Response.json({ error: "Use GET." }, { status: 405 });
|
|
1432
|
+
if (homeDir === void 0) throw new Error("Git review requires the admitted host home directory.");
|
|
1433
|
+
let recordedBaseRef;
|
|
1252
1434
|
try {
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1435
|
+
recordedBaseRef = (await createWorktreeRegistry(await registryFile(cwd, homeDir)).list()).find((record) => record.sessionId === context.sessionId)?.baseRef;
|
|
1436
|
+
} catch {
|
|
1437
|
+
recordedBaseRef = void 0;
|
|
1438
|
+
}
|
|
1439
|
+
const key = JSON.stringify([cwd, recordedBaseRef ?? null]);
|
|
1440
|
+
let pending = pendingReviews.get(key);
|
|
1441
|
+
if (pending === void 0) {
|
|
1442
|
+
pending = branchDiff.review(cwd, recordedBaseRef === void 0 ? {} : { recordedBaseRef }).finally(() => pendingReviews.delete(key));
|
|
1443
|
+
pendingReviews.set(key, pending);
|
|
1444
|
+
}
|
|
1445
|
+
const result = await pending;
|
|
1446
|
+
if (url.pathname === "/review") return Response.json(result?.summary ?? {
|
|
1447
|
+
repository: false,
|
|
1448
|
+
files: []
|
|
1449
|
+
});
|
|
1450
|
+
const filePath = url.searchParams.get("path") ?? "";
|
|
1451
|
+
if (result === void 0 || !result.summary.files.some((file) => file.path === filePath)) return Response.json({ error: "That file is not part of this review." }, { status: 404 });
|
|
1452
|
+
return Response.json(await branchDiff.fileDiff(result, filePath));
|
|
1453
|
+
};
|
|
1454
|
+
return {
|
|
1455
|
+
async fetch(request) {
|
|
1456
|
+
const url = new URL(request.url);
|
|
1457
|
+
try {
|
|
1458
|
+
const answered = await newSession(request, url) ?? await auth(request, url) ?? await review(request, url);
|
|
1459
|
+
if (answered !== void 0) return answered;
|
|
1460
|
+
} catch (error) {
|
|
1461
|
+
context.onNotice(`git request failed: ${error.name}`);
|
|
1462
|
+
return failed(error);
|
|
1264
1463
|
}
|
|
1265
|
-
const
|
|
1266
|
-
if (
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1464
|
+
const root = rootOf(url);
|
|
1465
|
+
if (root instanceof Response) return root;
|
|
1466
|
+
const callerContext = {
|
|
1467
|
+
cwd: root,
|
|
1468
|
+
sessionId: url.searchParams.get("sessionId") ?? ""
|
|
1469
|
+
};
|
|
1470
|
+
if ((request.method === "POST" && url.pathname === "/worktrees" || request.method === "DELETE" && /^\/worktrees\/[^/]+$/u.test(url.pathname)) && callerContext.sessionId === "") return badRequest("A sessionId is required for worktree mutations.");
|
|
1471
|
+
try {
|
|
1472
|
+
if (request.method === "GET" && url.pathname === "/worktrees") return Response.json({ worktrees: await operations.list(callerContext) });
|
|
1473
|
+
if (request.method === "POST" && url.pathname === "/worktrees") {
|
|
1474
|
+
const body = await request.json();
|
|
1475
|
+
if (typeof body.branch !== "string" || body.branch.trim() === "") return badRequest("A branch is required.");
|
|
1476
|
+
const record = await operations.spawn(callerContext, {
|
|
1477
|
+
branch: body.branch,
|
|
1478
|
+
...body.baseRef === void 0 ? {} : { baseRef: body.baseRef },
|
|
1479
|
+
...body.name === void 0 ? {} : { name: body.name }
|
|
1480
|
+
});
|
|
1481
|
+
publishLifecycle(callerContext.sessionId, record.repositoryRoot);
|
|
1482
|
+
return Response.json({ worktree: record }, { status: 201 });
|
|
1483
|
+
}
|
|
1484
|
+
const closing = /^\/worktrees\/([^/]+)$/u.exec(url.pathname);
|
|
1485
|
+
if (request.method === "DELETE" && closing) {
|
|
1486
|
+
const record = await operations.close(callerContext, decodeURIComponent(String(closing[1])), url.searchParams.get("force") === "true");
|
|
1487
|
+
publishLifecycle(callerContext.sessionId, record.repositoryRoot);
|
|
1488
|
+
return Response.json({ worktree: record });
|
|
1489
|
+
}
|
|
1490
|
+
return Response.json({ error: "No such worktree route." }, { status: 404 });
|
|
1491
|
+
} catch (error) {
|
|
1492
|
+
context.onNotice(`worktree request failed: ${error.name}`);
|
|
1493
|
+
return failed(error);
|
|
1270
1494
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1495
|
+
},
|
|
1496
|
+
close() {}
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
//#endregion
|
|
1502
|
+
//#region src/extensions/(backend)/api/git/route.server.ts
|
|
1503
|
+
var route_server_default = defineRoute(({ context, host, signal }) => {
|
|
1504
|
+
let openDirectory;
|
|
1505
|
+
context.inject([PO_SHELL_SERVICE], (child) => {
|
|
1506
|
+
const shell = requirePoShell(child);
|
|
1507
|
+
const env = Object.fromEntries(Object.entries(host.context.environment ?? {}).filter((entry) => entry[1] !== void 0));
|
|
1508
|
+
openDirectory = async (path) => {
|
|
1509
|
+
const result = await shell.exec([
|
|
1510
|
+
"/usr/bin/open",
|
|
1511
|
+
"-a",
|
|
1512
|
+
"Finder",
|
|
1513
|
+
path
|
|
1514
|
+
], {
|
|
1515
|
+
env,
|
|
1516
|
+
inheritEnv: false
|
|
1517
|
+
}, withAbortSignal(AbortSignal.any([signal, AbortSignal.timeout(5e3)]), BACKGROUND_CONTEXT));
|
|
1518
|
+
if (!result.ok || result.value.exitCode !== 0) throw new Error("Finder could not be opened.");
|
|
1278
1519
|
};
|
|
1279
|
-
|
|
1520
|
+
return () => {
|
|
1521
|
+
openDirectory = void 0;
|
|
1522
|
+
};
|
|
1523
|
+
});
|
|
1524
|
+
return createGitApi(async (path) => {
|
|
1525
|
+
if (openDirectory === void 0) throw new Error("The host shell is unavailable.");
|
|
1526
|
+
await openDirectory(path);
|
|
1527
|
+
});
|
|
1280
1528
|
});
|
|
1281
1529
|
//#endregion
|
|
1282
1530
|
//#region src/services/gitFailure/index.ts
|
|
@@ -1413,7 +1661,7 @@ async function rebasing(root) {
|
|
|
1413
1661
|
"--git-path",
|
|
1414
1662
|
name
|
|
1415
1663
|
]);
|
|
1416
|
-
if (gitPath !== void 0 && fs.
|
|
1664
|
+
if (gitPath !== void 0 && await fs.access(path.resolve(root, gitPath)).then(() => true, () => false)) return true;
|
|
1417
1665
|
}
|
|
1418
1666
|
return false;
|
|
1419
1667
|
}
|
|
@@ -1583,9 +1831,9 @@ const GIT_CHANGES_TYPE = "git_changes";
|
|
|
1583
1831
|
* DESIGN PATTERNS:
|
|
1584
1832
|
* - Apart from the worktrees channel. Worktrees are repository management;
|
|
1585
1833
|
* this is the session's own code change.
|
|
1586
|
-
* - Push, not poll. The session
|
|
1587
|
-
* tool call and settled turn; recounts are coalesced to one in
|
|
1588
|
-
* one rerun, so a burst of tool calls costs at most two.
|
|
1834
|
+
* - Push, not poll. The session's server plugin publishes a "changed" event
|
|
1835
|
+
* after every tool call and settled turn; recounts are coalesced to one in
|
|
1836
|
+
* flight plus one rerun, so a burst of tool calls costs at most two.
|
|
1589
1837
|
* - One sync command per session. A rebase and a push must never overlap.
|
|
1590
1838
|
* - Auth is read fresh for each command from the session's workspace, and goes
|
|
1591
1839
|
* only into that command's git child.
|
|
@@ -1646,9 +1894,9 @@ function createGitChangesChannel(options = {}) {
|
|
|
1646
1894
|
const rerun = /* @__PURE__ */ new Set();
|
|
1647
1895
|
const busy = /* @__PURE__ */ new Set();
|
|
1648
1896
|
/** The base the session's own worktree was created from, when it has one. */
|
|
1649
|
-
const recordedBaseRef = (scope) => {
|
|
1897
|
+
const recordedBaseRef = async (scope) => {
|
|
1650
1898
|
try {
|
|
1651
|
-
return createWorktreeRegistry(registryFile(scope.cwd, homeDir)).list().find((record) => record.sessionId === scope.sessionId)?.baseRef;
|
|
1899
|
+
return (await createWorktreeRegistry(await registryFile(scope.cwd, homeDir)).list()).find((record) => record.sessionId === scope.sessionId)?.baseRef;
|
|
1652
1900
|
} catch {
|
|
1653
1901
|
return;
|
|
1654
1902
|
}
|
|
@@ -1681,8 +1929,7 @@ function createGitChangesChannel(options = {}) {
|
|
|
1681
1929
|
return;
|
|
1682
1930
|
}
|
|
1683
1931
|
inFlight.add(sessionId);
|
|
1684
|
-
|
|
1685
|
-
diff.review(scope.cwd, base === void 0 ? {} : { recordedBaseRef: base }).then((review) => {
|
|
1932
|
+
recordedBaseRef(scope).then((base) => diff.review(scope.cwd, base === void 0 ? {} : { recordedBaseRef: base })).then((review) => {
|
|
1686
1933
|
if (scopes.has(sessionId)) update(sessionId, { changes: review?.changes });
|
|
1687
1934
|
}).catch((error) => {
|
|
1688
1935
|
host.onNotice(`git diff refresh failed for ${sessionId} (${error instanceof Error ? error.name : "unknown"})`);
|
|
@@ -1691,12 +1938,12 @@ function createGitChangesChannel(options = {}) {
|
|
|
1691
1938
|
if (rerun.delete(sessionId)) refresh(sessionId);
|
|
1692
1939
|
});
|
|
1693
1940
|
};
|
|
1694
|
-
const run = (scope, command) => {
|
|
1941
|
+
const run = async (scope, command) => {
|
|
1695
1942
|
const workspaceRoot = scope.sessionContext?.workspaceRoot ?? scope.cwd;
|
|
1696
|
-
const base = recordedBaseRef(scope);
|
|
1943
|
+
const base = await recordedBaseRef(scope);
|
|
1697
1944
|
const context = {
|
|
1698
1945
|
cwd: scope.cwd,
|
|
1699
|
-
auth: auth.read(workspaceRoot),
|
|
1946
|
+
auth: await auth.read(workspaceRoot),
|
|
1700
1947
|
...base === void 0 ? {} : { recordedBaseRef: base }
|
|
1701
1948
|
};
|
|
1702
1949
|
if (command.action === "pull") return sync.pull(context);
|
|
@@ -1794,9 +2041,9 @@ function payloadOf(state) {
|
|
|
1794
2041
|
};
|
|
1795
2042
|
}
|
|
1796
2043
|
/** Worktrees visible to this session, using the hub's live session table for ownership. */
|
|
1797
|
-
function viewsFor(scope, deps) {
|
|
2044
|
+
async function viewsFor(scope, deps) {
|
|
1798
2045
|
try {
|
|
1799
|
-
return createWorktreeRegistry(registryFile(scope.cwd, deps.homeDir)).list().flatMap((record) => {
|
|
2046
|
+
return (await createWorktreeRegistry(await registryFile(scope.cwd, deps.homeDir)).list()).flatMap((record) => {
|
|
1800
2047
|
if (record.parentSessionId === void 0 && record.sessionId !== scope.sessionId) return [];
|
|
1801
2048
|
const owned = record.parentSessionId === void 0 || record.parentSessionId === scope.sessionId;
|
|
1802
2049
|
const unowned = !owned && !deps.sessionService.isLive(record.parentSessionId);
|
|
@@ -1821,11 +2068,15 @@ function sameList(left, right) {
|
|
|
1821
2068
|
return other !== void 0 && entry.id === other.id && entry.branch === other.branch && entry.sessionId === other.sessionId && entry.orphaned === other.orphaned && entry.unowned === other.unowned;
|
|
1822
2069
|
});
|
|
1823
2070
|
}
|
|
1824
|
-
function errorTargetFor(command) {
|
|
1825
|
-
|
|
2071
|
+
function errorTargetFor(command, error) {
|
|
2072
|
+
if (command.action === "close") return {
|
|
1826
2073
|
action: "close",
|
|
1827
2074
|
id: command.id
|
|
1828
2075
|
};
|
|
2076
|
+
return error instanceof PoGitExpectedError && error.code === "rift_missing" ? {
|
|
2077
|
+
action: "create",
|
|
2078
|
+
riftMissing: true
|
|
2079
|
+
} : { action: "create" };
|
|
1829
2080
|
}
|
|
1830
2081
|
/** A command the page sent, or undefined when the frame was not one. */
|
|
1831
2082
|
function parseCommand(payload) {
|
|
@@ -1853,11 +2104,15 @@ function parseCommand(payload) {
|
|
|
1853
2104
|
/**
|
|
1854
2105
|
* What the reader is told when a command fails.
|
|
1855
2106
|
*
|
|
1856
|
-
* An expected failure explains itself
|
|
1857
|
-
*
|
|
2107
|
+
* An expected failure explains itself, minus the `[code]` tag and recovery line
|
|
2108
|
+
* written for the agent's tool result. A dirty close is said in the person's
|
|
2109
|
+
* terms, because its message tells the agent to pass `force`. Anything else is
|
|
2110
|
+
* summarised, never echoed: a git error can carry a remote URL with a token in it.
|
|
1858
2111
|
*/
|
|
1859
2112
|
function failureText(error) {
|
|
1860
|
-
|
|
2113
|
+
if (!(error instanceof PoGitExpectedError)) return "The worktree operation failed.";
|
|
2114
|
+
if (error.code === "worktree_dirty") return "The worktree has uncommitted changes.";
|
|
2115
|
+
return error.message.replace(/^\[[a-z_]+\] /u, "").split("\nRecovery:")[0] ?? error.message;
|
|
1861
2116
|
}
|
|
1862
2117
|
/** Starts one direct-event subscription per managed session. */
|
|
1863
2118
|
function createWorktreesChannel(options = {}) {
|
|
@@ -1910,10 +2165,13 @@ function createWorktreesChannel(options = {}) {
|
|
|
1910
2165
|
return generation;
|
|
1911
2166
|
};
|
|
1912
2167
|
/** Republishes a session, skipping a frame that would say nothing new. */
|
|
1913
|
-
const publish = (scope, force) => {
|
|
2168
|
+
const publish = async (scope, force) => {
|
|
2169
|
+
const generation = generations.get(scope.sessionId);
|
|
2170
|
+
const worktreeViews = await viewsFor(scope, views);
|
|
2171
|
+
if (generation === void 0 || generations.get(scope.sessionId) !== generation) return;
|
|
1914
2172
|
const previous = latest.get(scope.sessionId);
|
|
1915
2173
|
const next = {
|
|
1916
|
-
worktrees:
|
|
2174
|
+
worktrees: worktreeViews,
|
|
1917
2175
|
pending: previous?.pending,
|
|
1918
2176
|
error: previous?.error,
|
|
1919
2177
|
errorTarget: previous?.errorTarget
|
|
@@ -1925,7 +2183,7 @@ function createWorktreesChannel(options = {}) {
|
|
|
1925
2183
|
const mark = (scope, pending, error, errorTarget) => {
|
|
1926
2184
|
const previous = latest.get(scope.sessionId);
|
|
1927
2185
|
latest.set(scope.sessionId, {
|
|
1928
|
-
worktrees: previous?.worktrees ??
|
|
2186
|
+
worktrees: previous?.worktrees ?? [],
|
|
1929
2187
|
pending,
|
|
1930
2188
|
error,
|
|
1931
2189
|
errorTarget
|
|
@@ -1965,15 +2223,15 @@ function createWorktreesChannel(options = {}) {
|
|
|
1965
2223
|
sessionAdded(scope) {
|
|
1966
2224
|
newGeneration(scope.sessionId);
|
|
1967
2225
|
publish(scope, false);
|
|
1968
|
-
const unsubscribe = host.directEvents.subscribe(GIT_WORKTREE_LIFECYCLE_EVENT, scope.sessionId, (payload) => {
|
|
2226
|
+
const unsubscribe = host.directEvents.subscribe(GIT_WORKTREE_LIFECYCLE_EVENT, scope.sessionId, async (payload) => {
|
|
1969
2227
|
if (!isWorktreeLifecycleEvent(payload)) return;
|
|
1970
2228
|
let sameRepository = false;
|
|
1971
2229
|
try {
|
|
1972
|
-
sameRepository = registryFile(scope.cwd, options.homeDir) === registryFile(payload.repositoryRoot, options.homeDir);
|
|
2230
|
+
sameRepository = await registryFile(scope.cwd, options.homeDir) === await registryFile(payload.repositoryRoot, options.homeDir);
|
|
1973
2231
|
} catch {
|
|
1974
2232
|
sameRepository = false;
|
|
1975
2233
|
}
|
|
1976
|
-
if (sameRepository) publish(scope, false);
|
|
2234
|
+
if (sameRepository) await publish(scope, false);
|
|
1977
2235
|
});
|
|
1978
2236
|
subscriptions.set(scope.sessionId, unsubscribe);
|
|
1979
2237
|
},
|
|
@@ -2006,7 +2264,7 @@ function createWorktreesChannel(options = {}) {
|
|
|
2006
2264
|
}).catch((error) => {
|
|
2007
2265
|
if (generations.get(scope.sessionId) !== generation) return;
|
|
2008
2266
|
host.onNotice(`worktree command failed for ${scope.sessionId} (${failureText(error)})`);
|
|
2009
|
-
mark(scope, void 0, failureText(error), errorTargetFor(command));
|
|
2267
|
+
mark(scope, void 0, failureText(error), errorTargetFor(command, error));
|
|
2010
2268
|
}).finally(() => {
|
|
2011
2269
|
if (generations.get(scope.sessionId) === generation) busy.delete(scope.sessionId);
|
|
2012
2270
|
});
|
|
@@ -2028,20 +2286,8 @@ var agent_settled_server_default = defineHook((context) => context.root.hooks[1]
|
|
|
2028
2286
|
//#region src/extensions/workspaces/sessions/(backend)/hook/tool-execution-end.server.ts
|
|
2029
2287
|
var tool_execution_end_server_default = defineHook((context) => context.root.hooks[0]);
|
|
2030
2288
|
//#endregion
|
|
2031
|
-
//#region src/extensions/workspaces/sessions/(backend)/resource/_lib/poGit.server.ts
|
|
2032
|
-
var poGit_server_default$1 = (context) => context.root.resources[0];
|
|
2033
|
-
//#endregion
|
|
2034
|
-
//#region src/extensions/workspaces/sessions/(backend)/resource/poGit.server.ts
|
|
2035
|
-
var poGit_server_default = defineResource(poGit_server_default$1);
|
|
2036
|
-
//#endregion
|
|
2037
|
-
//#region src/extensions/workspaces/sessions/(backend)/resource/_lib/poGitReadme.server.ts
|
|
2038
|
-
var poGitReadme_server_default$1 = (context) => context.root.resources[2];
|
|
2039
|
-
//#endregion
|
|
2040
|
-
//#region src/extensions/workspaces/sessions/(backend)/resource/poGitReadme.server.ts
|
|
2041
|
-
var poGitReadme_server_default = defineResource(poGitReadme_server_default$1);
|
|
2042
|
-
//#endregion
|
|
2043
2289
|
//#region src/extensions/workspaces/sessions/(backend)/resource/_lib/poUseGit.server.ts
|
|
2044
|
-
var poUseGit_server_default$1 = (context) => context.root.resources[
|
|
2290
|
+
var poUseGit_server_default$1 = (context) => context.root.resources[0];
|
|
2045
2291
|
//#endregion
|
|
2046
2292
|
//#region src/extensions/workspaces/sessions/(backend)/resource/poUseGit.server.ts
|
|
2047
2293
|
var poUseGit_server_default = defineResource(poUseGit_server_default$1);
|
|
@@ -2051,19 +2297,22 @@ var runWorktree_server_default = defineServerTool((context) => context.root.tool
|
|
|
2051
2297
|
//#endregion
|
|
2052
2298
|
//#region generated/server.ts
|
|
2053
2299
|
const at = (value, context) => typeof value === "function" ? value(context) : value;
|
|
2300
|
+
/* v8 ignore next -- generated normalization branch */
|
|
2301
|
+
const defined = (values) => values.filter((value) => value !== void 0);
|
|
2054
2302
|
const via = (identity, value) => value === void 0 ? void 0 : {
|
|
2055
2303
|
...identity,
|
|
2056
2304
|
...value
|
|
2057
2305
|
};
|
|
2058
|
-
const
|
|
2306
|
+
const serverPlugin = defineServerPlugin({
|
|
2059
2307
|
name: "@agimon-ai/po-git",
|
|
2060
2308
|
providerOverrides: composeServerProviderOverrides(root_server_default),
|
|
2309
|
+
injects: composeRootInjects({ session: [root_server_default] }),
|
|
2061
2310
|
global: (context) => ({
|
|
2062
2311
|
get api() {
|
|
2063
2312
|
return [at(route_server_default, context)];
|
|
2064
2313
|
},
|
|
2065
2314
|
get channels() {
|
|
2066
|
-
return [() => via({ frameType: "git_changes" }, git_changes_server_default
|
|
2315
|
+
return [() => via({ frameType: "git_changes" }, at(git_changes_server_default, context)), () => via({ frameType: "git_worktrees" }, at(git_worktrees_server_default, context))];
|
|
2067
2316
|
}
|
|
2068
2317
|
}),
|
|
2069
2318
|
workspace: (context) => ({
|
|
@@ -2071,11 +2320,11 @@ const facet = defineServerPlugin({
|
|
|
2071
2320
|
return [at(route_server_default, context)];
|
|
2072
2321
|
},
|
|
2073
2322
|
get channels() {
|
|
2074
|
-
return [() => via({ frameType: "git_changes" }, git_changes_server_default
|
|
2323
|
+
return defined([context.hosted("global") ? void 0 : () => via({ frameType: "git_changes" }, at(git_changes_server_default, context)), context.hosted("global") ? void 0 : () => via({ frameType: "git_worktrees" }, at(git_worktrees_server_default, context))]);
|
|
2075
2324
|
}
|
|
2076
2325
|
}),
|
|
2077
2326
|
session: async (context) => {
|
|
2078
|
-
const scopeSession = await root_server_default
|
|
2327
|
+
const scopeSession = await context.scopeRoot("session", root_server_default, context);
|
|
2079
2328
|
const contextSession = {
|
|
2080
2329
|
...context,
|
|
2081
2330
|
root: scopeSession.value
|
|
@@ -2086,17 +2335,13 @@ const facet = defineServerPlugin({
|
|
|
2086
2335
|
return [at(route_server_default, context)];
|
|
2087
2336
|
},
|
|
2088
2337
|
get channels() {
|
|
2089
|
-
return [() => via({ frameType: "git_changes" }, git_changes_server_default
|
|
2338
|
+
return defined([context.hosted("global") ? void 0 : () => via({ frameType: "git_changes" }, at(git_changes_server_default, context)), context.hosted("global") ? void 0 : () => via({ frameType: "git_worktrees" }, at(git_worktrees_server_default, context))]);
|
|
2090
2339
|
},
|
|
2091
2340
|
get hooks() {
|
|
2092
2341
|
return [via({ event: "agent_settled" }, at(agent_settled_server_default, contextSession)), via({ event: "tool_execution_end" }, at(tool_execution_end_server_default, contextSession))];
|
|
2093
2342
|
},
|
|
2094
2343
|
get resources() {
|
|
2095
|
-
return [
|
|
2096
|
-
at(poGit_server_default, contextSession),
|
|
2097
|
-
at(poGitReadme_server_default, contextSession),
|
|
2098
|
-
at(poUseGit_server_default, contextSession)
|
|
2099
|
-
];
|
|
2344
|
+
return [at(poUseGit_server_default, contextSession)];
|
|
2100
2345
|
},
|
|
2101
2346
|
get tools() {
|
|
2102
2347
|
return [via({ name: "run_worktree" }, at(runWorktree_server_default, contextSession))];
|
|
@@ -2109,6 +2354,6 @@ const facet = defineServerPlugin({
|
|
|
2109
2354
|
}
|
|
2110
2355
|
});
|
|
2111
2356
|
//#endregion
|
|
2112
|
-
export {
|
|
2357
|
+
export { serverPlugin as default, serverPlugin };
|
|
2113
2358
|
|
|
2114
2359
|
//# sourceMappingURL=server.mjs.map
|