@dungsil/dsh-plugin-ui-vcs 0.2.0 → 0.4.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.
Files changed (3) hide show
  1. package/dist/client.js +448 -141
  2. package/dist/index.js +510 -112
  3. package/package.json +7 -1
package/dist/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { clientRequestSchema } from "@deepseek-ai/dsh-client-connection";
3
- import { SessionId } from "@deepseek-ai/dsh-session";
3
+ import { Session, SessionId } from "@deepseek-ai/dsh-session";
4
4
  import { Buffer as Buffer$1 } from "node:buffer";
5
5
  import { Context, Service } from "@deepseek-ai/cordis";
6
+ import { createHash } from "node:crypto";
7
+ import { homedir } from "node:os";
6
8
  // \0rolldown/runtime.js
7
9
  var __defProp = Object.defineProperty;
8
10
  var __exportAll = (all, no_symbols) => {
@@ -899,19 +901,82 @@ defineMethod("transform", [
899
901
  "callback",
900
902
  "preserve"
901
903
  ], ({ inner }, isInner) => inner.toString(isInner));
904
+ // src/inspect-session.ts
905
+ async function inspectSession(ctx, id, signal) {
906
+ signal.throwIfAborted();
907
+ const live = ctx.get("agents")?.get(id);
908
+ if (live) return live.session;
909
+ const inspection = await ctx.sessionController.inspect(id, signal);
910
+ signal.throwIfAborted();
911
+ return Session.create(id, inspection.events, inspection.meta, inspection.inheritedEventCount);
912
+ }
913
+ // ../../tools/async/src/index.ts
914
+ function createTaskQueue({ concurrency, maxPending }) {
915
+ if (!Number.isSafeInteger(concurrency) || concurrency < 1 || !Number.isSafeInteger(maxPending) || maxPending < 0) throw new RangeError("Invalid task queue limits");
916
+ let active = 0;
917
+ const pending = /* @__PURE__ */ new Set();
918
+ const drain = () => {
919
+ while (active < concurrency && pending.size) pending.values().next().value.start();
920
+ };
921
+ return (signal, task) => {
922
+ if (signal.aborted) return Promise.reject(signal.reason);
923
+ if (active >= concurrency && pending.size >= maxPending) return Promise.reject(/* @__PURE__ */ new Error("busy"));
924
+ return new Promise((resolve, reject) => {
925
+ const entry = {
926
+ start() {
927
+ pending.delete(entry);
928
+ signal.removeEventListener("abort", entry.cancel);
929
+ active++;
930
+ (async () => {
931
+ try {
932
+ signal.throwIfAborted();
933
+ resolve(await task());
934
+ } catch (error) {
935
+ reject(error);
936
+ } finally {
937
+ active--;
938
+ drain();
939
+ }
940
+ })();
941
+ },
942
+ cancel() {
943
+ pending.delete(entry);
944
+ signal.removeEventListener("abort", entry.cancel);
945
+ reject(signal.reason);
946
+ }
947
+ };
948
+ pending.add(entry);
949
+ signal.addEventListener("abort", entry.cancel, { once: true });
950
+ drain();
951
+ });
952
+ };
953
+ }
954
+ // src/read-queue.ts
955
+ const queues = /* @__PURE__ */ new WeakMap();
956
+ function readQueue(provider) {
957
+ let queue = queues.get(provider);
958
+ if (!queue) {
959
+ queue = createTaskQueue({
960
+ concurrency: 2,
961
+ maxPending: 128
962
+ });
963
+ queues.set(provider, queue);
964
+ }
965
+ return queue;
966
+ }
902
967
  // src/pull-requests-protocol.ts
903
968
  const channel$1 = "/api";
904
969
  const method = "dungsil-vcs/pull-requests/list";
905
970
  const actionMethod = "dungsil-vcs/pull-requests/action";
906
971
  // src/pull-requests.ts
907
972
  var pull_requests_exports = /* @__PURE__ */ __exportAll({
908
- Config: () => Config,
909
- apply: () => apply$2,
910
- inject: () => inject$2,
911
- name: () => name$2
973
+ Config: () => Config$1,
974
+ apply: () => apply$3,
975
+ inject: () => inject$3,
976
+ name: () => name$3
912
977
  });
913
- const name$2 = "dungsil-vcs-pull-requests";
914
- const inject$2 = [
978
+ const name$3 = "dungsil-vcs-pull-requests";
979
+ const inject$3 = [
915
980
  "connection",
916
981
  "sessionController",
917
982
  "workingDirectory",
@@ -919,7 +984,7 @@ const inject$2 = [
919
984
  "sandbox",
920
985
  "sandboxPolicy"
921
986
  ];
922
- const Config = Schema.object({ provider: Schema.union([
987
+ const Config$1 = Schema.object({ provider: Schema.union([
923
988
  "auto",
924
989
  "github",
925
990
  "gitlab",
@@ -971,12 +1036,13 @@ function actionInput$1(value) {
971
1036
  ].includes(input.strategy)) || input.action === "merge" && input.body !== void 0) throw new Error("invalid_payload");
972
1037
  return input;
973
1038
  }
974
- function apply$2(ctx, config) {
1039
+ function apply$3(ctx, config) {
975
1040
  const lifetime = new AbortController();
976
1041
  const actionsInFlight = /* @__PURE__ */ new Set();
1042
+ const read = readQueue(ctx.subprocess);
977
1043
  ctx.effect(() => () => lifetime.abort());
978
- const list = async (agent, signal, action, requested, summary = false) => {
979
- const cwd = ctx.workingDirectory.get(agent.session);
1044
+ const list = async (session, signal, action, requested, summary = false) => {
1045
+ const cwd = ctx.workingDirectory.get(session);
980
1046
  const run = async (executable, args, failure = "command_failed", allowed = [0], trim = true) => {
981
1047
  let resolved;
982
1048
  try {
@@ -985,7 +1051,7 @@ function apply$2(ctx, config) {
985
1051
  throw new Error(executable === "git" ? "repository" : "cli_missing");
986
1052
  }
987
1053
  const argv = [resolved, ...args];
988
- const policy = ctx.sandboxPolicy.resolve({ session: agent.session });
1054
+ const policy = ctx.sandboxPolicy.resolve({ session });
989
1055
  const command = policy.mode === "danger-full-access" ? argv : (await ctx.sandbox.confine(argv, {
990
1056
  ...policy,
991
1057
  mode: policy.mode
@@ -1118,7 +1184,7 @@ function apply$2(ctx, config) {
1118
1184
  if (requested.origin !== new URL(origin).origin || provider === "github" && repository.split("/").length !== 2 || !match || !Number.isSafeInteger(Number(match[2]))) throw new Error("unsupported");
1119
1185
  if (provider === "github" ? match[1].toLowerCase() !== prefix.toLowerCase() : match[1] !== prefix) {
1120
1186
  if (provider === "gitlab") throw new Error("unsupported");
1121
- const associated = await list(agent, signal, void 0, void 0, summary);
1187
+ const associated = await list(session, signal, void 0, void 0, summary);
1122
1188
  const linkedUrl = `${requested.origin}${match[1]}/${provider === "github" ? "pull" : forge ? "pulls" : "-/merge_requests"}/${match[2]}`;
1123
1189
  if ("request" in associated && associated.request && (provider === "github" ? associated.request.url.toLowerCase() === linkedUrl.toLowerCase() : associated.request.url === linkedUrl)) return associated;
1124
1190
  throw new Error("unsupported");
@@ -1713,14 +1779,24 @@ function apply$2(ctx, config) {
1713
1779
  inFlight = action.sessionId;
1714
1780
  }
1715
1781
  const requested = route === "dungsil-vcs/pull-requests/list" && input.requestUrl !== void 0 ? lookupUrl(input.requestUrl) : void 0;
1716
- const resolved = await ctx.sessionController.resolveAgent(SessionId(input.sessionId));
1717
- if ("error" in resolved) throw new Error("unavailable");
1718
1782
  const signal = AbortSignal.any([
1719
1783
  request.signal,
1720
1784
  lifetime.signal,
1721
1785
  AbortSignal.timeout(2e4)
1722
1786
  ]);
1723
- const value = await list(resolved.agent, signal, action, requested, input.summary === true);
1787
+ const sessionId = SessionId(input.sessionId);
1788
+ const execute = async () => {
1789
+ signal.throwIfAborted();
1790
+ let session;
1791
+ if (action) {
1792
+ const resolved = await ctx.sessionController.resolveAgent(sessionId);
1793
+ if ("error" in resolved) throw new Error("unavailable");
1794
+ signal.throwIfAborted();
1795
+ session = resolved.agent.session;
1796
+ } else session = await inspectSession(ctx, sessionId, signal);
1797
+ return list(session, signal, action, requested, input.summary === true);
1798
+ };
1799
+ const value = action ? await execute() : await read(signal, execute);
1724
1800
  if (action && !("completed" in value)) throw new Error("stale_request");
1725
1801
  return reply({
1726
1802
  ok: true,
@@ -4514,14 +4590,23 @@ async function generateTexts(ctx, agent, operation, context, signal) {
4514
4590
  // src/protocol.ts
4515
4591
  const channel = "/api";
4516
4592
  const executeMethod = "dungsil-vcs/execute";
4593
+ function validCommitSettings(value) {
4594
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
4595
+ const input = value;
4596
+ return Object.keys(input).every((key) => [
4597
+ "authorName",
4598
+ "authorEmail",
4599
+ "noSign"
4600
+ ].includes(key)) && typeof input.authorName === "string" && input.authorName.length <= 200 && !/[<>]/.test(input.authorName) && !Array.from(input.authorName).some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127) && typeof input.authorEmail === "string" && input.authorEmail.length <= 254 && (input.authorEmail === "" || /^[^\s<>@]+@[^\s<>@]+$/.test(input.authorEmail)) && !Array.from(input.authorEmail).some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127) && (input.authorName === "" ? input.authorEmail === "" : !!input.authorName.trim() && !!input.authorEmail) && typeof input.noSign === "boolean";
4601
+ }
4517
4602
  // src/commands.ts
4518
4603
  var commands_exports = /* @__PURE__ */ __exportAll({
4519
- apply: () => apply$1,
4520
- inject: () => inject$1,
4521
- name: () => name$1
4604
+ apply: () => apply$2,
4605
+ inject: () => inject$2,
4606
+ name: () => name$2
4522
4607
  });
4523
- const name$1 = "dungsil-vcs-commands";
4524
- const inject$1 = [
4608
+ const name$2 = "dungsil-vcs-commands";
4609
+ const inject$2 = [
4525
4610
  "connection",
4526
4611
  "sessionController",
4527
4612
  "workingDirectory",
@@ -4546,7 +4631,7 @@ function actionInput(value) {
4546
4631
  ].includes(input.operation) || typeof input.requestId !== "string" || !/^[a-zA-Z0-9-]{16,100}$/.test(input.requestId)) throw new Error("invalid");
4547
4632
  return input;
4548
4633
  }
4549
- function apply$1(ctx, config) {
4634
+ function apply$2(ctx, config) {
4550
4635
  const lifetime = new AbortController();
4551
4636
  const pending = /* @__PURE__ */ new Set();
4552
4637
  const repositories = /* @__PURE__ */ new Set();
@@ -4561,6 +4646,12 @@ function apply$1(ctx, config) {
4561
4646
  }
4562
4647
  });
4563
4648
  const execute = async (agent, input, signal) => {
4649
+ const settings = {
4650
+ authorName: config.authorName ?? "",
4651
+ authorEmail: config.authorEmail ?? "",
4652
+ noSign: config.noSign ?? false
4653
+ };
4654
+ if (!validCommitSettings(settings)) throw new Error("invalid");
4564
4655
  if (ctx.sandboxPolicy.resolve({ session: agent.session }).mode === "read-only") throw new Error("read_only");
4565
4656
  const cwd = await ctx.workingDirectory.ensure(agent, signal);
4566
4657
  if (cwd !== input.cwd) throw new Error("conflict");
@@ -4810,6 +4901,8 @@ function apply$1(ctx, config) {
4810
4901
  ]);
4811
4902
  await git([
4812
4903
  "commit",
4904
+ ...settings.authorName ? [`--author=${settings.authorName} <${settings.authorEmail}>`] : [],
4905
+ ...settings.noSign ? ["--no-gpg-sign"] : [],
4813
4906
  "--only",
4814
4907
  "-m",
4815
4908
  texts.commitMessage,
@@ -4906,6 +4999,303 @@ function apply$1(ctx, config) {
4906
4999
  }
4907
5000
  }), "ui-vcs: owner-only commands");
4908
5001
  }
5002
+ // src/cli-status.ts
5003
+ function object(value) {
5004
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid_status");
5005
+ return value;
5006
+ }
5007
+ function parse(output) {
5008
+ if (output.length > 1048576) throw new Error("invalid_status");
5009
+ return JSON.parse(output);
5010
+ }
5011
+ function safe(value) {
5012
+ return typeof value === "string" && !/(?:gh[pousr]_|github_pat_|gl(?:pat|oas|rt|cbt|ptt|ft|soat|dt)-|Bearer|eyJ)/i.test(value) && !/[a-f0-9]{20,}/i.test(value) && !/[a-z0-9_+-]{32,}/i.test(value);
5013
+ }
5014
+ function host(value) {
5015
+ if (!safe(value) || value.length > 253 || !/^(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?::[0-9]{1,5})?$/i.test(value)) throw new Error("invalid_identity");
5016
+ return value.toLowerCase();
5017
+ }
5018
+ function account(hostname, login, source) {
5019
+ if (!safe(login) || !/^[a-z0-9][a-z0-9_.-]{0,99}$/i.test(login)) throw new Error("invalid_identity");
5020
+ const result = {
5021
+ host: host(hostname),
5022
+ login
5023
+ };
5024
+ if ([
5025
+ "GH_TOKEN",
5026
+ "GITHUB_TOKEN",
5027
+ "GH_ENTERPRISE_TOKEN",
5028
+ "GITHUB_ENTERPRISE_TOKEN",
5029
+ "environment"
5030
+ ].includes(String(source))) result.method = "environment";
5031
+ else if (source === "keyring") result.method = "keyring";
5032
+ else if (source === "config_file" || typeof source === "string" && /(?:^|[/\\])hosts\.yml$/.test(source)) result.method = "config-file";
5033
+ return result;
5034
+ }
5035
+ async function inspectAuth(cli, run) {
5036
+ const data = object(parse(await run(cli, cli === "gh" ? [
5037
+ "auth",
5038
+ "status",
5039
+ "--json",
5040
+ "hosts"
5041
+ ] : [
5042
+ "auth",
5043
+ "status",
5044
+ "--all",
5045
+ "--output",
5046
+ "json"
5047
+ ])));
5048
+ const rows = cli === "gh" ? Object.values(object(data.hosts)).flatMap((entries) => {
5049
+ if (!Array.isArray(entries)) throw new Error("invalid_status");
5050
+ return entries;
5051
+ }) : data.hosts;
5052
+ if (!Array.isArray(rows)) throw new Error("invalid_status");
5053
+ const accounts = [];
5054
+ let failed = false;
5055
+ for (const row of rows) {
5056
+ const entry = object(row);
5057
+ if (entry.state === "success") try {
5058
+ accounts.push(account(entry.host, cli === "gh" ? entry.login : entry.user, cli === "gh" ? entry.tokenSource : entry.token_source));
5059
+ } catch {
5060
+ failed = true;
5061
+ }
5062
+ else if (entry.state !== "refresh_expired") failed = true;
5063
+ }
5064
+ return {
5065
+ cli,
5066
+ status: failed ? "error" : accounts.length ? "authenticated" : "unauthenticated",
5067
+ accounts
5068
+ };
5069
+ }
5070
+ async function inspectTea(run) {
5071
+ const logins = parse(await run("tea", [
5072
+ "login",
5073
+ "list",
5074
+ "--output",
5075
+ "json"
5076
+ ]));
5077
+ if (!Array.isArray(logins)) throw new Error("invalid_status");
5078
+ const accounts = [];
5079
+ let failed = false;
5080
+ for (const row of logins) try {
5081
+ const entry = object(row);
5082
+ if (typeof entry.name !== "string" || !entry.name || entry.name.length > 256 || entry.name.split("").some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127)) throw new Error("invalid_status");
5083
+ if (typeof entry.url !== "string") throw new Error("invalid_status");
5084
+ const url = new URL(entry.url);
5085
+ if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new Error("invalid_identity");
5086
+ const hostname = host(url.host);
5087
+ const user = object(parse(await run("tea", [
5088
+ "api",
5089
+ "--method",
5090
+ "GET",
5091
+ "--login",
5092
+ entry.name,
5093
+ "user"
5094
+ ])));
5095
+ if (typeof user.id !== "number" || !Number.isSafeInteger(user.id) || user.id <= 0) throw new Error("invalid_user");
5096
+ accounts.push(account(hostname, user.login));
5097
+ } catch (error) {
5098
+ if (error instanceof Error && error.message === "cli_missing") throw error;
5099
+ failed = true;
5100
+ }
5101
+ return {
5102
+ cli: "tea",
5103
+ status: failed ? "error" : accounts.length ? "authenticated" : "unauthenticated",
5104
+ accounts
5105
+ };
5106
+ }
5107
+ /** The runner must reject resolution failures with Error('cli_missing'), and
5108
+ * reject nonzero exits, policy failures and timeouts with any other error.
5109
+ * It must bound execution/output and disable prompts. No stdout/stderr is ever
5110
+ * returned, including when older CLIs do not support these structured queries.
5111
+ */
5112
+ async function inspectCliStatus(run) {
5113
+ return Promise.all([
5114
+ "gh",
5115
+ "glab",
5116
+ "tea"
5117
+ ].map(async (cli) => {
5118
+ try {
5119
+ return await (cli === "tea" ? inspectTea(run) : inspectAuth(cli, run));
5120
+ } catch (error) {
5121
+ return {
5122
+ cli,
5123
+ status: error instanceof Error && error.message === "cli_missing" ? "unavailable" : "error",
5124
+ accounts: []
5125
+ };
5126
+ }
5127
+ }));
5128
+ }
5129
+ // src/settings.ts
5130
+ var settings_exports = /* @__PURE__ */ __exportAll({
5131
+ Config: () => Config,
5132
+ apply: () => apply$1,
5133
+ inject: () => inject$1,
5134
+ name: () => name$1
5135
+ });
5136
+ const name$1 = "dungsil-vcs-settings";
5137
+ const inject$1 = [
5138
+ "connection",
5139
+ "configEditor",
5140
+ "subprocess",
5141
+ "sandbox",
5142
+ "sandboxPolicy"
5143
+ ];
5144
+ const Config = Schema.object({
5145
+ provider: Schema.union([
5146
+ "auto",
5147
+ "github",
5148
+ "gitlab",
5149
+ "gitea",
5150
+ "forgejo"
5151
+ ]).default("auto"),
5152
+ authorName: Schema.string().default(""),
5153
+ authorEmail: Schema.string().default(""),
5154
+ noSign: Schema.boolean().default(false)
5155
+ });
5156
+ function apply$1(ctx) {
5157
+ const editor = ctx.configEditor;
5158
+ const lifetime = new AbortController();
5159
+ ctx.effect(() => () => lifetime.abort());
5160
+ const entry = () => {
5161
+ const entries = editor.entries().filter((item) => item.options.name === "@dungsil/dsh-plugin-ui-vcs");
5162
+ if (entries.length !== 1) throw new Error("unavailable");
5163
+ return entries[0];
5164
+ };
5165
+ const snapshot = () => {
5166
+ const current = entry().options.config;
5167
+ return {
5168
+ revision: createHash("sha256").update(JSON.stringify(current ?? {})).digest("hex"),
5169
+ settings: {
5170
+ authorName: current?.authorName ?? "",
5171
+ authorEmail: current?.authorEmail ?? "",
5172
+ noSign: current?.noSign ?? false
5173
+ }
5174
+ };
5175
+ };
5176
+ for (const endpoint of [
5177
+ "settings",
5178
+ "save-settings",
5179
+ "cli-status"
5180
+ ]) {
5181
+ const method = `dungsil-vcs/${endpoint}`;
5182
+ ctx.effect(() => ctx.connection.fetch.register({
5183
+ path: `${channel}/${method}`,
5184
+ methods: ["POST"],
5185
+ requestBody: "buffered",
5186
+ async fetch(request) {
5187
+ let rpcId = "";
5188
+ let result;
5189
+ try {
5190
+ const body = clientRequestSchema.parse(await request.json());
5191
+ rpcId = body.rpcId;
5192
+ if (body.method !== method) throw new Error("invalid");
5193
+ const signal = AbortSignal.any([
5194
+ request.signal,
5195
+ lifetime.signal,
5196
+ AbortSignal.timeout(3e4)
5197
+ ]);
5198
+ signal.throwIfAborted();
5199
+ if (endpoint === "settings") result = {
5200
+ ok: true,
5201
+ value: snapshot()
5202
+ };
5203
+ else if (endpoint === "save-settings") {
5204
+ const input = body.payload;
5205
+ if (!input || typeof input.revision !== "string" || !validCommitSettings(input.settings)) throw new Error("invalid");
5206
+ const settings = input.settings;
5207
+ await editor.edit(entry(), (current) => {
5208
+ if (snapshot().revision !== input.revision) throw new Error("conflict");
5209
+ return {
5210
+ ...current,
5211
+ ...settings
5212
+ };
5213
+ });
5214
+ result = {
5215
+ ok: true,
5216
+ value: snapshot()
5217
+ };
5218
+ } else {
5219
+ const value = await inspectCliStatus(async (executable, args) => {
5220
+ const bounded = AbortSignal.any([signal, AbortSignal.timeout(1e4)]);
5221
+ let path;
5222
+ try {
5223
+ path = await ctx.subprocess.resolveExecutable(executable, void 0, bounded);
5224
+ } catch (error) {
5225
+ if (error instanceof Error && error.name === "SubprocessExecutableNotFoundError") throw new Error("cli_missing");
5226
+ throw error;
5227
+ }
5228
+ const argv = [path, ...args];
5229
+ const policy = ctx.sandboxPolicy.resolve();
5230
+ const command = policy.mode === "danger-full-access" ? argv : (await ctx.sandbox.confine(argv, {
5231
+ ...policy,
5232
+ mode: policy.mode
5233
+ }, bounded)).argv;
5234
+ const child = ctx.subprocess.spawn({
5235
+ argv: command,
5236
+ cwd: homedir(),
5237
+ signal: bounded,
5238
+ graceMs: 1e3,
5239
+ env: {
5240
+ GH_PROMPT_DISABLED: "1",
5241
+ GLAB_PROMPT_DISABLED: "1",
5242
+ NO_COLOR: "1",
5243
+ GH_HOST: void 0,
5244
+ GH_REPO: void 0,
5245
+ GITLAB_HOST: void 0,
5246
+ GITLAB_REPO: void 0,
5247
+ TEA_LOGIN: void 0,
5248
+ TEA_REPO: void 0,
5249
+ ...executable === "tea" ? {
5250
+ GH_TOKEN: void 0,
5251
+ GITEA_TOKEN: void 0,
5252
+ GITEA_INSTANCE_URL: void 0,
5253
+ GITEA_INSTANCE_SSH_HOST: void 0,
5254
+ GITEA_INSTANCE_INSECURE: void 0
5255
+ } : {}
5256
+ },
5257
+ stdio: {
5258
+ stdin: "ignore",
5259
+ stdout: { maxBytes: 131072 },
5260
+ stderr: { maxBytes: 16384 }
5261
+ }
5262
+ });
5263
+ try {
5264
+ const outcome = await child.done;
5265
+ bounded.throwIfAborted();
5266
+ const output = child.collected.stdout.readFrom(0);
5267
+ if (outcome.exitCode !== 0 || output.lossy) throw new Error("command_failed");
5268
+ return output.text;
5269
+ } finally {
5270
+ child.terminate();
5271
+ await child.waitForExit();
5272
+ }
5273
+ });
5274
+ signal.throwIfAborted();
5275
+ result = {
5276
+ ok: true,
5277
+ value
5278
+ };
5279
+ }
5280
+ } catch (error) {
5281
+ result = {
5282
+ ok: false,
5283
+ error: {
5284
+ code: error instanceof Error && ["invalid", "conflict"].includes(error.message) ? error.message : "unavailable",
5285
+ message: "Could not confirm VCS settings.",
5286
+ details: {}
5287
+ }
5288
+ };
5289
+ }
5290
+ return Response.json({
5291
+ type: "server-response",
5292
+ rpcId,
5293
+ result
5294
+ }, { headers: { "Cache-Control": "no-store" } });
5295
+ }
5296
+ }), `ui-vcs: owner-only ${endpoint}`);
5297
+ }
5298
+ }
4909
5299
  // src/index.ts
4910
5300
  const name = "dungsil-ui-vcs";
4911
5301
  const inject = [
@@ -4917,8 +5307,12 @@ const inject = [
4917
5307
  "workingDirectory"
4918
5308
  ];
4919
5309
  function apply(ctx, config) {
5310
+ const lifetime = new AbortController();
5311
+ const read = readQueue(ctx.subprocess);
5312
+ ctx.effect(() => () => lifetime.abort(), "ui-vcs: cancel context reads");
4920
5313
  ctx.plugin(pull_requests_exports, config);
4921
5314
  ctx.plugin(commands_exports, config);
5315
+ ctx.plugin(settings_exports);
4922
5316
  ctx.effect(() => ctx.connection.fetch.register({
4923
5317
  path: "/api/dungsil-vcs/describe",
4924
5318
  methods: ["POST"],
@@ -4940,100 +5334,104 @@ function apply(ctx, config) {
4940
5334
  if (body.method !== "dungsil-vcs/describe") throw new Error("Invalid method");
4941
5335
  const sessionId = body.payload?.sessionId;
4942
5336
  if (typeof sessionId !== "string" || !sessionId) throw new Error("Invalid session");
4943
- const resolved = await ctx.sessionController.resolveAgent(SessionId(sessionId));
4944
- if ("error" in resolved) throw new Error("Session unavailable");
4945
- const { agent } = resolved;
4946
- const signal = AbortSignal.any([request.signal, AbortSignal.timeout(1e4)]);
4947
- const cwd = await ctx.workingDirectory.ensure(agent, signal);
4948
- const git = async (args) => {
4949
- const argv = [
4950
- await ctx.subprocess.resolveExecutable("git", void 0, signal),
4951
- "-c",
4952
- "core.fsmonitor=false",
4953
- "-c",
4954
- "core.hooksPath=/dev/null",
4955
- ...args
4956
- ];
4957
- const policy = ctx.sandboxPolicy.resolve({ session: agent.session });
4958
- const command = policy.mode === "danger-full-access" ? argv : (await ctx.sandbox.confine(argv, {
4959
- ...policy,
4960
- mode: policy.mode
4961
- }, signal)).argv;
4962
- const child = ctx.subprocess.spawn({
4963
- argv: command,
4964
- cwd,
4965
- signal,
4966
- graceMs: 1e3,
4967
- env: {
4968
- ...Object.fromEntries(Object.keys(process.env).filter((key) => key.startsWith("GIT_")).map((key) => [key, void 0])),
4969
- GIT_TERMINAL_PROMPT: "0",
4970
- GIT_OPTIONAL_LOCKS: "0",
4971
- GIT_NO_LAZY_FETCH: "1"
4972
- },
4973
- stdio: {
4974
- stdin: "ignore",
4975
- stdout: { maxBytes: 65536 },
4976
- stderr: { maxBytes: 4096 }
4977
- }
4978
- });
4979
- try {
4980
- const outcome = await child.done;
4981
- signal.throwIfAborted();
4982
- const output = child.collected.stdout.readFrom(0);
4983
- if (outcome.exitCode !== 0 || output.lossy) throw new Error("Git read failed");
4984
- return output.text.replace(/\r?\n$/, "");
4985
- } finally {
4986
- child.terminate();
4987
- await child.waitForExit();
4988
- }
4989
- };
4990
- const gitDir = await git([
4991
- "rev-parse",
4992
- "--path-format=absolute",
4993
- "--git-dir"
4994
- ]);
4995
- const commonDir = await git([
4996
- "rev-parse",
4997
- "--path-format=absolute",
4998
- "--git-common-dir"
5337
+ const signal = AbortSignal.any([
5338
+ request.signal,
5339
+ lifetime.signal,
5340
+ AbortSignal.timeout(1e4)
4999
5341
  ]);
5000
- let operation = "pr";
5001
- if (gitDir === commonDir) {
5002
- const branch = await git([
5003
- "symbolic-ref",
5004
- "--quiet",
5005
- "--short",
5006
- "HEAD"
5342
+ result = await read(signal, async () => {
5343
+ const session = await inspectSession(ctx, SessionId(sessionId), signal);
5344
+ const cwd = ctx.workingDirectory.get(session);
5345
+ const git = async (args) => {
5346
+ const argv = [
5347
+ await ctx.subprocess.resolveExecutable("git", void 0, signal),
5348
+ "-c",
5349
+ "core.fsmonitor=false",
5350
+ "-c",
5351
+ "core.hooksPath=/dev/null",
5352
+ ...args
5353
+ ];
5354
+ const policy = ctx.sandboxPolicy.resolve({ session });
5355
+ const command = policy.mode === "danger-full-access" ? argv : (await ctx.sandbox.confine(argv, {
5356
+ ...policy,
5357
+ mode: policy.mode
5358
+ }, signal)).argv;
5359
+ const child = ctx.subprocess.spawn({
5360
+ argv: command,
5361
+ cwd,
5362
+ signal,
5363
+ graceMs: 1e3,
5364
+ env: {
5365
+ ...Object.fromEntries(Object.keys(process.env).filter((key) => key.startsWith("GIT_")).map((key) => [key, void 0])),
5366
+ GIT_TERMINAL_PROMPT: "0",
5367
+ GIT_OPTIONAL_LOCKS: "0",
5368
+ GIT_NO_LAZY_FETCH: "1"
5369
+ },
5370
+ stdio: {
5371
+ stdin: "ignore",
5372
+ stdout: { maxBytes: 65536 },
5373
+ stderr: { maxBytes: 4096 }
5374
+ }
5375
+ });
5376
+ try {
5377
+ const outcome = await child.done;
5378
+ signal.throwIfAborted();
5379
+ const output = child.collected.stdout.readFrom(0);
5380
+ if (outcome.exitCode !== 0 || output.lossy) throw new Error("Git read failed");
5381
+ return output.text.replace(/\r?\n$/, "");
5382
+ } finally {
5383
+ child.terminate();
5384
+ await child.waitForExit();
5385
+ }
5386
+ };
5387
+ const gitDir = await git([
5388
+ "rev-parse",
5389
+ "--path-format=absolute",
5390
+ "--git-dir"
5007
5391
  ]);
5008
- const upstream = await git([
5009
- "for-each-ref",
5010
- "--format=%(upstream:remotename)",
5011
- `refs/heads/${branch}`
5392
+ const commonDir = await git([
5393
+ "rev-parse",
5394
+ "--path-format=absolute",
5395
+ "--git-common-dir"
5012
5396
  ]);
5013
- const remotes = (await git(["remote"])).split("\n").filter(Boolean);
5014
- const remote = upstream && upstream !== "." ? upstream : remotes.includes("origin") ? "origin" : remotes.length === 1 ? remotes[0] : void 0;
5015
- if (!remote) throw new Error("Default remote unknown");
5016
- operation = await git([
5017
- "symbolic-ref",
5018
- "--quiet",
5019
- `refs/remotes/${remote}/HEAD`
5020
- ]) === `refs/remotes/${remote}/${branch}` ? "push" : "pr";
5021
- }
5022
- const headSha = await git(["rev-parse", "HEAD"]);
5023
- const dirty = !!await git([
5024
- "status",
5025
- "--porcelain",
5026
- "--untracked-files=normal"
5027
- ]);
5028
- result = {
5029
- ok: true,
5030
- value: {
5031
- operation,
5032
- headSha,
5033
- dirty
5397
+ let operation = "pr";
5398
+ if (gitDir === commonDir) {
5399
+ const branch = await git([
5400
+ "symbolic-ref",
5401
+ "--quiet",
5402
+ "--short",
5403
+ "HEAD"
5404
+ ]);
5405
+ const upstream = await git([
5406
+ "for-each-ref",
5407
+ "--format=%(upstream:remotename)",
5408
+ `refs/heads/${branch}`
5409
+ ]);
5410
+ const remotes = (await git(["remote"])).split("\n").filter(Boolean);
5411
+ const remote = upstream && upstream !== "." ? upstream : remotes.includes("origin") ? "origin" : remotes.length === 1 ? remotes[0] : void 0;
5412
+ if (!remote) throw new Error("Default remote unknown");
5413
+ operation = await git([
5414
+ "symbolic-ref",
5415
+ "--quiet",
5416
+ `refs/remotes/${remote}/HEAD`
5417
+ ]) === `refs/remotes/${remote}/${branch}` ? "push" : "pr";
5034
5418
  }
5035
- };
5036
- if (ctx.workingDirectory.get(agent.session) !== cwd) throw new Error("Working directory changed");
5419
+ const headSha = await git(["rev-parse", "HEAD"]);
5420
+ const dirty = !!await git([
5421
+ "status",
5422
+ "--porcelain",
5423
+ "--untracked-files=normal"
5424
+ ]);
5425
+ if (ctx.workingDirectory.get(session) !== cwd) throw new Error("Working directory changed");
5426
+ return {
5427
+ ok: true,
5428
+ value: {
5429
+ operation,
5430
+ headSha,
5431
+ dirty
5432
+ }
5433
+ };
5434
+ });
5037
5435
  } catch {
5038
5436
  result = failure;
5039
5437
  }