@agimon-ai/po-git 0.0.0-alpha.78 → 0.0.0-alpha.79

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.
@@ -14,6 +14,8 @@ let _agimon_ai_po_core_extensionFile = require("@agimon-ai/po-core/extensionFile
14
14
  let _agimon_ai_po_core_codemode = require("@agimon-ai/po-core/codemode");
15
15
  let _agimon_ai_po_session = require("@agimon-ai/po-session");
16
16
  let typebox = require("typebox");
17
+ let _agimon_ai_po_core_platform = require("@agimon-ai/po-core/platform");
18
+ let _agimon_ai_po_core_packageApi = require("@agimon-ai/po-core/packageApi");
17
19
  //#region src/schemas/runWorktreeTool.ts
18
20
  /**
19
21
  * The tool's action vocabulary.
@@ -1292,177 +1294,236 @@ function failed(error) {
1292
1294
  }, { status: error.retryable ? 503 : 409 });
1293
1295
  return Response.json({ error: "The worktree operation failed." }, { status: 500 });
1294
1296
  }
1295
- //#endregion
1296
- //#region src/extensions/(backend)/api/git/route.server.ts
1297
- var route_server_default = (0, _agimon_ai_po_core_extensionFile.defineRoute)({
1298
- basePath: "git",
1299
- start(context) {
1300
- if (context.directEvents === void 0) throw new Error("Git hub API requires the direct event bus.");
1301
- const directEvents = context.directEvents;
1302
- const publishLifecycle = (sessionId, repositoryRoot) => {
1303
- directEvents.publish(require_worktreeOperations.GIT_WORKTREE_LIFECYCLE_EVENT, sessionId, {
1304
- version: 1,
1305
- repositoryRoot
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(require_worktreeOperations.GIT_WORKTREE_LIFECYCLE_EVENT, sessionId, {
1305
+ version: 1,
1306
+ repositoryRoot
1307
+ });
1308
+ };
1309
+ const git = require_worktreeOperations.createWorktreeGit();
1310
+ const operations = require_worktreeOperations.createWorktreeOperations({
1311
+ git,
1312
+ sessionService: context.sessionService,
1313
+ ...context.homeDirectory === void 0 ? {} : { homeDir: context.homeDirectory }
1306
1314
  });
1307
- };
1308
- const git = require_worktreeOperations.createWorktreeGit();
1309
- const operations = require_worktreeOperations.createWorktreeOperations({
1310
- git,
1311
- sessionService: context.sessionService,
1312
- ...context.homeDirectory === void 0 ? {} : { homeDir: context.homeDirectory }
1313
- });
1314
- /**
1315
- * The new-session dialog's routes: the workspace's branches, and a top-level
1316
- * worktree session on one. Workspace-mounted only, and always rooted at the
1317
- * mount's own workspace, so a request can never name another directory.
1318
- */
1319
- const newSession = async (request, url) => {
1320
- const isBranches = request.method === "GET" && url.pathname === "/branches";
1321
- const isCreate = request.method === "POST" && url.pathname === "/sessions";
1322
- if (!isBranches && !isCreate) return void 0;
1323
- const workspaceRoot = context.scope === "workspace" ? context.workspaceRoot : void 0;
1324
- if (workspaceRoot === void 0) return Response.json({ error: "This route belongs to a workspace." }, { status: 404 });
1325
- if (isBranches) {
1326
- const repositoryRoot = await git.repositoryRoot(workspaceRoot);
1327
- const branches = repositoryRoot === void 0 ? {
1328
- repository: false,
1329
- local: [],
1330
- remote: []
1331
- } : {
1332
- repository: true,
1333
- ...await git.listBranches(repositoryRoot)
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 && (0, _agimon_ai_po_core_packageApi.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" || !require_worktreeOperations.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
+ });
1376
+ publishLifecycle(record.sessionId, record.repositoryRoot);
1377
+ const created = {
1378
+ sessionId: record.sessionId,
1379
+ worktreeId: record.id
1334
1380
  };
1335
- return Response.json(branches);
1336
- }
1337
- const parsed = newSessionRequest(await request.json().catch(() => void 0));
1338
- if ("error" in parsed) return badRequest(parsed.error);
1339
- const record = await operations.spawn({ cwd: workspaceRoot }, {
1340
- branch: parsed.branch,
1341
- checkout: parsed.mode === "existing-branch" ? "existing" : "new",
1342
- ...parsed.mode === "existing-branch" && parsed.remote !== void 0 ? { remote: parsed.remote } : {},
1343
- ...parsed.mode === "new-branch" && parsed.baseRef !== void 0 ? { baseRef: parsed.baseRef } : {},
1344
- ...parsed.name === void 0 ? {} : { name: parsed.name }
1345
- });
1346
- publishLifecycle(record.sessionId, record.repositoryRoot);
1347
- const created = {
1348
- sessionId: record.sessionId,
1349
- worktreeId: record.id
1381
+ return Response.json(created, { status: 201 });
1350
1382
  };
1351
- return Response.json(created, { status: 201 });
1352
- };
1353
- /** The admitted root for a request, or a response explaining why not. */
1354
- const rootOf = (url) => {
1355
- const repositoryId = url.searchParams.get("repositoryId");
1356
- if (repositoryId === null || repositoryId === "") return badRequest("A repositoryId is required.");
1357
- const root = context.resolveRepository?.(repositoryId);
1358
- if (root === void 0) return badRequest("That repository is not admitted to this cockpit.");
1359
- return root;
1360
- };
1361
- const homeDir = context.homeDirectory ?? node_os.default.homedir();
1362
- const gitAuth = createGitAuthStore(homeDir);
1363
- const branchDiff = createBranchDiff();
1364
- const pendingReviews = /* @__PURE__ */ new Map();
1365
- /**
1366
- * A workspace's remote auth. Workspace mounts only, so the step-up gate on
1367
- * `PUT /api/workspaces/<id>/plugins/git/auth` is the one way to write it.
1368
- * The view never carries the token.
1369
- */
1370
- const auth = async (request, url) => {
1371
- if (url.pathname !== "/auth") return void 0;
1372
- if (context.scope !== "workspace") return Response.json({ error: "This route belongs to a workspace." }, { status: 404 });
1373
- if (request.method !== "GET" && request.method !== "PUT") return Response.json({ error: "Use GET or PUT." }, { status: 405 });
1374
- const root = rootOf(url);
1375
- if (root instanceof Response) return root;
1376
- if (request.method === "GET") return Response.json(await gitAuth.view(root));
1377
- try {
1378
- return Response.json(await gitAuth.save(root, await request.json().catch(() => void 0)));
1379
- } catch (error) {
1380
- if (error instanceof require_worktreeOperations.PoGitExpectedError && error.code === "invalid_request") {
1381
- const text = error.message.replace(/^\[[a-z_]+\] /u, "").replace("\nRecovery: ", " ");
1382
- return Response.json({
1383
- error: text,
1384
- code: error.code
1385
- }, { status: 400 });
1386
- }
1387
- throw error;
1388
- }
1389
- };
1390
- /**
1391
- * The session's change against its base. Session mounts only, rooted at the
1392
- * session's own checkout; a file is served only when it is in the change set
1393
- * computed for the request. Overlapping requests share an in-flight snapshot.
1394
- */
1395
- const review = async (request, url) => {
1396
- if (url.pathname !== "/review" && url.pathname !== "/review/file") return void 0;
1397
- const cwd = context.scope === "session" ? context.cwd : void 0;
1398
- if (cwd === void 0) return Response.json({ error: "This route belongs to a session." }, { status: 404 });
1399
- if (request.method !== "GET") return Response.json({ error: "Use GET." }, { status: 405 });
1400
- let recordedBaseRef;
1401
- try {
1402
- recordedBaseRef = (await require_worktreeOperations.createWorktreeRegistry(await require_worktreeOperations.registryFile(cwd, homeDir)).list()).find((record) => record.sessionId === context.sessionId)?.baseRef;
1403
- } catch {
1404
- recordedBaseRef = void 0;
1405
- }
1406
- const key = JSON.stringify([cwd, recordedBaseRef ?? null]);
1407
- let pending = pendingReviews.get(key);
1408
- if (pending === void 0) {
1409
- pending = branchDiff.review(cwd, recordedBaseRef === void 0 ? {} : { recordedBaseRef }).finally(() => pendingReviews.delete(key));
1410
- pendingReviews.set(key, pending);
1411
- }
1412
- const result = await pending;
1413
- if (url.pathname === "/review") return Response.json(result?.summary ?? {
1414
- repository: false,
1415
- files: []
1416
- });
1417
- const filePath = url.searchParams.get("path") ?? "";
1418
- 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 });
1419
- return Response.json(await branchDiff.fileDiff(result, filePath));
1420
- };
1421
- return {
1422
- async fetch(request) {
1423
- const url = new URL(request.url);
1383
+ /** The admitted root for a request, or a response explaining why not. */
1384
+ const rootOf = (url) => {
1385
+ const repositoryId = url.searchParams.get("repositoryId");
1386
+ if (repositoryId === null || repositoryId === "") return badRequest("A repositoryId is required.");
1387
+ const root = context.resolveRepository?.(repositoryId);
1388
+ if (root === void 0) return badRequest("That repository is not admitted to this cockpit.");
1389
+ return root;
1390
+ };
1391
+ const homeDir = context.homeDirectory ?? context.environment?.HOME;
1392
+ const gitAuth = homeDir === void 0 ? void 0 : createGitAuthStore(homeDir);
1393
+ const branchDiff = createBranchDiff();
1394
+ const pendingReviews = /* @__PURE__ */ new Map();
1395
+ /**
1396
+ * A workspace's remote auth. Workspace mounts only, so the step-up gate on
1397
+ * `PUT /api/workspaces/<id>/plugins/git/auth` is the one way to write it.
1398
+ * The view never carries the token.
1399
+ */
1400
+ const auth = async (request, url) => {
1401
+ if (url.pathname !== "/auth") return void 0;
1402
+ if (context.scope !== "workspace") return Response.json({ error: "This route belongs to a workspace." }, { status: 404 });
1403
+ if (request.method !== "GET" && request.method !== "PUT") return Response.json({ error: "Use GET or PUT." }, { status: 405 });
1404
+ if (gitAuth === void 0) throw new Error("Git auth requires the admitted host home directory.");
1405
+ const root = rootOf(url);
1406
+ if (root instanceof Response) return root;
1407
+ if (request.method === "GET") return Response.json(await gitAuth.view(root));
1424
1408
  try {
1425
- const answered = await newSession(request, url) ?? await auth(request, url) ?? await review(request, url);
1426
- if (answered !== void 0) return answered;
1409
+ return Response.json(await gitAuth.save(root, await request.json().catch(() => void 0)));
1427
1410
  } catch (error) {
1428
- context.onNotice(`git request failed: ${error.name}`);
1429
- return failed(error);
1411
+ if (error instanceof require_worktreeOperations.PoGitExpectedError && error.code === "invalid_request") {
1412
+ const text = error.message.replace(/^\[[a-z_]+\] /u, "").replace("\nRecovery: ", " ");
1413
+ return Response.json({
1414
+ error: text,
1415
+ code: error.code
1416
+ }, { status: 400 });
1417
+ }
1418
+ throw error;
1430
1419
  }
1431
- const root = rootOf(url);
1432
- if (root instanceof Response) return root;
1433
- const callerContext = {
1434
- cwd: root,
1435
- sessionId: url.searchParams.get("sessionId") ?? ""
1436
- };
1437
- 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.");
1420
+ };
1421
+ /**
1422
+ * The session's change against its base. Session mounts only, rooted at the
1423
+ * session's own checkout; a file is served only when it is in the change set
1424
+ * computed for the request. Overlapping requests share an in-flight snapshot.
1425
+ */
1426
+ const review = async (request, url) => {
1427
+ if (url.pathname !== "/review" && url.pathname !== "/review/file") return void 0;
1428
+ const cwd = context.scope === "session" ? context.cwd : void 0;
1429
+ if (cwd === void 0) return Response.json({ error: "This route belongs to a session." }, { status: 404 });
1430
+ if (request.method !== "GET") return Response.json({ error: "Use GET." }, { status: 405 });
1431
+ if (homeDir === void 0) throw new Error("Git review requires the admitted host home directory.");
1432
+ let recordedBaseRef;
1438
1433
  try {
1439
- if (request.method === "GET" && url.pathname === "/worktrees") return Response.json({ worktrees: await operations.list(callerContext) });
1440
- if (request.method === "POST" && url.pathname === "/worktrees") {
1441
- const body = await request.json();
1442
- if (typeof body.branch !== "string" || body.branch.trim() === "") return badRequest("A branch is required.");
1443
- const record = await operations.spawn(callerContext, {
1444
- branch: body.branch,
1445
- ...body.baseRef === void 0 ? {} : { baseRef: body.baseRef },
1446
- ...body.name === void 0 ? {} : { name: body.name }
1447
- });
1448
- publishLifecycle(callerContext.sessionId, record.repositoryRoot);
1449
- return Response.json({ worktree: record }, { status: 201 });
1434
+ recordedBaseRef = (await require_worktreeOperations.createWorktreeRegistry(await require_worktreeOperations.registryFile(cwd, homeDir)).list()).find((record) => record.sessionId === context.sessionId)?.baseRef;
1435
+ } catch {
1436
+ recordedBaseRef = void 0;
1437
+ }
1438
+ const key = JSON.stringify([cwd, recordedBaseRef ?? null]);
1439
+ let pending = pendingReviews.get(key);
1440
+ if (pending === void 0) {
1441
+ pending = branchDiff.review(cwd, recordedBaseRef === void 0 ? {} : { recordedBaseRef }).finally(() => pendingReviews.delete(key));
1442
+ pendingReviews.set(key, pending);
1443
+ }
1444
+ const result = await pending;
1445
+ if (url.pathname === "/review") return Response.json(result?.summary ?? {
1446
+ repository: false,
1447
+ files: []
1448
+ });
1449
+ const filePath = url.searchParams.get("path") ?? "";
1450
+ 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 });
1451
+ return Response.json(await branchDiff.fileDiff(result, filePath));
1452
+ };
1453
+ return {
1454
+ async fetch(request) {
1455
+ const url = new URL(request.url);
1456
+ try {
1457
+ const answered = await newSession(request, url) ?? await auth(request, url) ?? await review(request, url);
1458
+ if (answered !== void 0) return answered;
1459
+ } catch (error) {
1460
+ context.onNotice(`git request failed: ${error.name}`);
1461
+ return failed(error);
1450
1462
  }
1451
- const closing = /^\/worktrees\/([^/]+)$/u.exec(url.pathname);
1452
- if (request.method === "DELETE" && closing) {
1453
- const record = await operations.close(callerContext, decodeURIComponent(String(closing[1])), url.searchParams.get("force") === "true");
1454
- publishLifecycle(callerContext.sessionId, record.repositoryRoot);
1455
- return Response.json({ worktree: record });
1463
+ const root = rootOf(url);
1464
+ if (root instanceof Response) return root;
1465
+ const callerContext = {
1466
+ cwd: root,
1467
+ sessionId: url.searchParams.get("sessionId") ?? ""
1468
+ };
1469
+ 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.");
1470
+ try {
1471
+ if (request.method === "GET" && url.pathname === "/worktrees") return Response.json({ worktrees: await operations.list(callerContext) });
1472
+ if (request.method === "POST" && url.pathname === "/worktrees") {
1473
+ const body = await request.json();
1474
+ if (typeof body.branch !== "string" || body.branch.trim() === "") return badRequest("A branch is required.");
1475
+ const record = await operations.spawn(callerContext, {
1476
+ branch: body.branch,
1477
+ ...body.baseRef === void 0 ? {} : { baseRef: body.baseRef },
1478
+ ...body.name === void 0 ? {} : { name: body.name }
1479
+ });
1480
+ publishLifecycle(callerContext.sessionId, record.repositoryRoot);
1481
+ return Response.json({ worktree: record }, { status: 201 });
1482
+ }
1483
+ const closing = /^\/worktrees\/([^/]+)$/u.exec(url.pathname);
1484
+ if (request.method === "DELETE" && closing) {
1485
+ const record = await operations.close(callerContext, decodeURIComponent(String(closing[1])), url.searchParams.get("force") === "true");
1486
+ publishLifecycle(callerContext.sessionId, record.repositoryRoot);
1487
+ return Response.json({ worktree: record });
1488
+ }
1489
+ return Response.json({ error: "No such worktree route." }, { status: 404 });
1490
+ } catch (error) {
1491
+ context.onNotice(`worktree request failed: ${error.name}`);
1492
+ return failed(error);
1456
1493
  }
1457
- return Response.json({ error: "No such worktree route." }, { status: 404 });
1458
- } catch (error) {
1459
- context.onNotice(`worktree request failed: ${error.name}`);
1460
- return failed(error);
1461
- }
1462
- },
1463
- close() {}
1494
+ },
1495
+ close() {}
1496
+ };
1497
+ }
1498
+ };
1499
+ }
1500
+ //#endregion
1501
+ //#region src/extensions/(backend)/api/git/route.server.ts
1502
+ var route_server_default = (0, _agimon_ai_po_core_extensionFile.defineRoute)(({ context, host, signal }) => {
1503
+ let openDirectory;
1504
+ context.inject([_agimon_ai_po_core_platform.PO_SHELL_SERVICE], (child) => {
1505
+ const shell = (0, _agimon_ai_po_core_platform.requirePoShell)(child);
1506
+ const env = Object.fromEntries(Object.entries(host.context.environment ?? {}).filter((entry) => entry[1] !== void 0));
1507
+ openDirectory = async (path) => {
1508
+ const result = await shell.exec([
1509
+ "/usr/bin/open",
1510
+ "-a",
1511
+ "Finder",
1512
+ path
1513
+ ], {
1514
+ env,
1515
+ inheritEnv: false
1516
+ }, (0, _agimon_ai_po_core_platform.withAbortSignal)(AbortSignal.any([signal, AbortSignal.timeout(5e3)]), _agimon_ai_po_core_platform.BACKGROUND_CONTEXT));
1517
+ if (!result.ok || result.value.exitCode !== 0) throw new Error("Finder could not be opened.");
1464
1518
  };
1465
- }
1519
+ return () => {
1520
+ openDirectory = void 0;
1521
+ };
1522
+ });
1523
+ return createGitApi(async (path) => {
1524
+ if (openDirectory === void 0) throw new Error("The host shell is unavailable.");
1525
+ await openDirectory(path);
1526
+ });
1466
1527
  });
1467
1528
  //#endregion
1468
1529
  //#region src/services/gitFailure/index.ts