@ghfs/cli 0.0.3 → 0.0.4

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/dist/cli.mjs CHANGED
@@ -1,20 +1,23 @@
1
1
  #!/usr/bin/env node
2
- import { a as formatIssueNumber, i as formatDuration, n as normalizeReactions, o as formatTerminalLink$1, r as countNoun, s as formatValue, t as createRepositoryProvider } from "./factory-COZFMWsb.mjs";
2
+ import { a as formatIssueNumber, i as formatDuration, n as normalizeReactions, o as formatTerminalLink$1, r as countNoun, s as formatValue, t as createRepositoryProvider } from "./factory-DYHQBeCz.mjs";
3
3
  import process from "node:process";
4
4
  import { cac } from "cac";
5
- import { basename, dirname, isAbsolute, join, resolve } from "pathe";
5
+ import { basename, dirname, extname, isAbsolute, join, normalize, resolve } from "pathe";
6
6
  import { execFile } from "node:child_process";
7
7
  import { promisify } from "node:util";
8
8
  import { existsSync } from "node:fs";
9
9
  import { createJiti } from "jiti";
10
- import { access, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
10
+ import { access, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
11
11
  import * as v from "valibot";
12
12
  import { parse, stringify } from "yaml";
13
13
  import { randomBytes } from "node:crypto";
14
14
  import c from "ansis";
15
15
  import * as p from "@clack/prompts";
16
16
  import { cancel, confirm, isCancel, multiselect, password, select } from "@clack/prompts";
17
-
17
+ import { createServer } from "node:http";
18
+ import { createBirpcGroup } from "birpc";
19
+ import { parse as parse$1, stringify as stringify$1 } from "structured-clone-es";
20
+ import { WebSocketServer } from "ws";
18
21
  //#region src/config/auth.ts
19
22
  const execFileAsync$1 = promisify(execFile);
20
23
  async function resolveAuthToken(options) {
@@ -44,7 +47,6 @@ async function readTokenFromEnv() {
44
47
  if (value) return value;
45
48
  }
46
49
  }
47
-
48
50
  //#endregion
49
51
  //#region src/constants.ts
50
52
  const CONFIG_FILE_CANDIDATES = [
@@ -54,7 +56,6 @@ const CONFIG_FILE_CANDIDATES = [
54
56
  "ghfs.config.js",
55
57
  "ghfs.config.cjs"
56
58
  ];
57
- const DEFAULT_STORAGE_DIR = ".ghfs";
58
59
  const ISSUE_DIR_NAME = "issues";
59
60
  const PULL_DIR_NAME = "pulls";
60
61
  const CLOSED_DIR_NAME = "closed";
@@ -65,7 +66,6 @@ const REPO_SNAPSHOT_FILE_NAME = "repo.json";
65
66
  const EXECUTE_FILE_NAME = "execute.yml";
66
67
  const EXECUTE_MD_FILE_NAME = "execute.md";
67
68
  const EXECUTE_SCHEMA_RELATIVE_PATH = "schema/execute.schema.json";
68
-
69
69
  //#endregion
70
70
  //#region src/config/load.ts
71
71
  async function loadUserConfig(cwd) {
@@ -90,7 +90,7 @@ async function resolveConfig(options = {}) {
90
90
  const overrides = options.overrides ?? {};
91
91
  const { config: userConfig } = await loadUserConfig(cwd);
92
92
  const merged = mergeUserConfig(userConfig, overrides);
93
- const directory = merged.directory ?? DEFAULT_STORAGE_DIR;
93
+ const directory = merged.directory ?? ".ghfs";
94
94
  const configuredToken = merged.auth?.token?.trim() || "";
95
95
  const repo = merged.repo?.trim() || "";
96
96
  const issuesEnabled = merged.sync?.issues ?? true;
@@ -136,7 +136,6 @@ function mergeUserConfig(base, overrides) {
136
136
  }
137
137
  };
138
138
  }
139
-
140
139
  //#endregion
141
140
  //#region src/utils/fs.ts
142
141
  async function pathExists(path) {
@@ -179,7 +178,6 @@ async function removePatchIfExists(storageDirAbsolute, number) {
179
178
  }
180
179
  return removed;
181
180
  }
182
-
183
181
  //#endregion
184
182
  //#region src/config/repo.ts
185
183
  const execFileAsync = promisify(execFile);
@@ -318,7 +316,6 @@ function prioritizeRemotes(remotes) {
318
316
  function stripGitSuffix(name) {
319
317
  return name.replace(/\.git$/, "");
320
318
  }
321
-
322
319
  //#endregion
323
320
  //#region src/execute/actions.ts
324
321
  const ACTIONS_SUPPORTED = [
@@ -400,7 +397,6 @@ function resolveActionName(action) {
400
397
  function normalizeActionInput(action) {
401
398
  return action.trim().toLowerCase();
402
399
  }
403
-
404
400
  //#endregion
405
401
  //#region src/execute/schema.ts
406
402
  const executeSchema = {
@@ -450,23 +446,22 @@ const executeSchema = {
450
446
  };
451
447
  const EXECUTE_FILE_PLACEHOLDER = [
452
448
  `# yaml-language-server: $schema=./${EXECUTE_SCHEMA_RELATIVE_PATH}`,
453
- "# Add operations as YAML list items, then run: ghfs execute --run",
454
- "# Action names are case-insensitive and support aliases (for example: label, assign, comment, close-comment).",
449
+ "# Add operations as YAML list items, then run: `ghfs execute`, examples:",
450
+ "#",
455
451
  "# - action: close",
456
452
  "# number: 123",
457
- "[]",
458
453
  ""
459
454
  ].join("\n");
460
455
  const EXECUTE_MD_FILE_PLACEHOLDER = [
461
- "# Add one action per line, then run: ghfs execute --run",
462
- "# Command names are case-insensitive and support aliases.",
463
- "# close #123 #124",
464
- "# label #123 bug, triage",
465
- "# assign #123 antfu",
466
- "# comment #123 \"Need more context\"",
467
- "# close-comment #123 \"Closing as completed\"",
468
- "# set-title #123 \"new title\"",
469
- "# add-tag #123 foo, bar",
456
+ "<!-- Add one action per line, then run: `ghfs execute`, examples: -->",
457
+ "",
458
+ "<!-- close #123 #124 -->",
459
+ "<!-- label #123 bug, triage -->",
460
+ "<!-- assign #123 antfu -->",
461
+ "<!-- comment #123 \"Need more context\" -->",
462
+ "<!-- close-comment #123 \"Closing as completed\" -->",
463
+ "<!-- set-title #123 \"new title\" -->",
464
+ "<!-- add-tag #123 foo, bar -->",
470
465
  ""
471
466
  ].join("\n");
472
467
  async function writeExecuteSchema(storageDirAbsolute) {
@@ -506,7 +501,6 @@ async function ensureExecuteMdFile(storageDirAbsolute) {
506
501
  function getExecuteSchemaPath(storageDirAbsolute) {
507
502
  return join(storageDirAbsolute, EXECUTE_SCHEMA_RELATIVE_PATH);
508
503
  }
509
-
510
504
  //#endregion
511
505
  //#region src/execute/validate.ts
512
506
  const executeOpSchema = v.looseObject({
@@ -532,7 +526,7 @@ async function readAndValidateExecuteFileWithSource(path) {
532
526
  const raw = await readFile(path, "utf8");
533
527
  let parsed;
534
528
  try {
535
- parsed = parse(raw);
529
+ parsed = parse(raw || "[]") || [];
536
530
  } catch (error) {
537
531
  throw new Error(`Failed to parse execute YAML: ${error.message}`);
538
532
  }
@@ -629,7 +623,6 @@ function normalizeActionInputs(pending) {
629
623
  actionErrors
630
624
  };
631
625
  }
632
-
633
626
  //#endregion
634
627
  //#region src/execute/sources/execute-md.ts
635
628
  const MULTI_SIMPLE_ACTIONS = new Set([
@@ -933,16 +926,13 @@ function tokenizeCommand(value) {
933
926
  function isCommentLine(trimmed) {
934
927
  return trimmed.startsWith("#") || trimmed.startsWith("//") || trimmed.startsWith("<!--");
935
928
  }
936
-
937
929
  //#endregion
938
930
  //#region package.json
939
- var version = "0.0.3";
940
-
931
+ var version = "0.0.4";
941
932
  //#endregion
942
933
  //#region src/meta.ts
943
934
  const GHFS_NAME = "ghfs";
944
935
  const GHFS_VERSION = version;
945
-
946
936
  //#endregion
947
937
  //#region src/sync/state.ts
948
938
  function getSyncStatePath(storageDirAbsolute) {
@@ -1034,7 +1024,143 @@ function normalizeItem(item) {
1034
1024
  }
1035
1025
  };
1036
1026
  }
1037
-
1027
+ //#endregion
1028
+ //#region src/execute/diff.ts
1029
+ function computeExecuteDiffOps(options) {
1030
+ const ops = [];
1031
+ const ifUnchangedSince = options.ifUnchangedSince;
1032
+ const current = normalizeDiffFields(options.current);
1033
+ const desired = normalizeDiffFields(options.desired);
1034
+ if (current.title !== desired.title) ops.push({
1035
+ action: "set-title",
1036
+ number: options.number,
1037
+ title: desired.title,
1038
+ ifUnchangedSince
1039
+ });
1040
+ if (options.includeBody && current.body !== desired.body && desired.body) ops.push({
1041
+ action: "set-body",
1042
+ number: options.number,
1043
+ body: desired.body,
1044
+ ifUnchangedSince
1045
+ });
1046
+ if (current.state !== desired.state) ops.push({
1047
+ action: desired.state === "closed" ? "close" : "reopen",
1048
+ number: options.number,
1049
+ ifUnchangedSince
1050
+ });
1051
+ if (!sameStringSet(current.labels, desired.labels)) {
1052
+ const additions = diffStrings(desired.labels, current.labels);
1053
+ const deletions = diffStrings(current.labels, desired.labels);
1054
+ if (additions.length > 0 && deletions.length > 0) ops.push({
1055
+ action: "set-labels",
1056
+ number: options.number,
1057
+ labels: desired.labels,
1058
+ ifUnchangedSince
1059
+ });
1060
+ else if (additions.length > 0) ops.push({
1061
+ action: "add-labels",
1062
+ number: options.number,
1063
+ labels: additions,
1064
+ ifUnchangedSince
1065
+ });
1066
+ else if (deletions.length > 0) ops.push({
1067
+ action: "remove-labels",
1068
+ number: options.number,
1069
+ labels: deletions,
1070
+ ifUnchangedSince
1071
+ });
1072
+ }
1073
+ if (!sameStringSet(current.assignees, desired.assignees)) {
1074
+ if (desired.assignees.length > 0) ops.push({
1075
+ action: "set-assignees",
1076
+ number: options.number,
1077
+ assignees: desired.assignees,
1078
+ ifUnchangedSince
1079
+ });
1080
+ else if (current.assignees.length > 0) ops.push({
1081
+ action: "remove-assignees",
1082
+ number: options.number,
1083
+ assignees: current.assignees,
1084
+ ifUnchangedSince
1085
+ });
1086
+ }
1087
+ if (current.milestone !== desired.milestone) if (desired.milestone) ops.push({
1088
+ action: "set-milestone",
1089
+ number: options.number,
1090
+ milestone: desired.milestone,
1091
+ ifUnchangedSince
1092
+ });
1093
+ else ops.push({
1094
+ action: "clear-milestone",
1095
+ number: options.number,
1096
+ ifUnchangedSince
1097
+ });
1098
+ if (!sameStringSet(current.reviewers, desired.reviewers)) {
1099
+ const additions = diffStrings(desired.reviewers, current.reviewers);
1100
+ const deletions = diffStrings(current.reviewers, desired.reviewers);
1101
+ if (additions.length > 0) ops.push({
1102
+ action: "request-reviewers",
1103
+ number: options.number,
1104
+ reviewers: additions,
1105
+ ifUnchangedSince
1106
+ });
1107
+ if (deletions.length > 0) ops.push({
1108
+ action: "remove-reviewers",
1109
+ number: options.number,
1110
+ reviewers: deletions,
1111
+ ifUnchangedSince
1112
+ });
1113
+ }
1114
+ if (typeof current.isDraft === "boolean" && typeof desired.isDraft === "boolean" && current.isDraft !== desired.isDraft) ops.push({
1115
+ action: desired.isDraft ? "convert-to-draft" : "mark-ready-for-review",
1116
+ number: options.number,
1117
+ ifUnchangedSince
1118
+ });
1119
+ return ops;
1120
+ }
1121
+ function normalizeStringArray(value) {
1122
+ if (!Array.isArray(value)) return [];
1123
+ const unique = /* @__PURE__ */ new Set();
1124
+ for (const entry of value) {
1125
+ if (typeof entry !== "string") continue;
1126
+ const normalized = entry.trim();
1127
+ if (!normalized) continue;
1128
+ unique.add(normalized);
1129
+ }
1130
+ return [...unique];
1131
+ }
1132
+ function normalizeMilestone(value) {
1133
+ if (typeof value !== "string") return null;
1134
+ const normalized = value.trim();
1135
+ return normalized.length > 0 ? normalized : null;
1136
+ }
1137
+ function normalizeBody(value) {
1138
+ if (value == null) return null;
1139
+ const trimmed = value.trim();
1140
+ return trimmed.length > 0 ? trimmed : null;
1141
+ }
1142
+ function normalizeDiffFields(fields) {
1143
+ return {
1144
+ ...fields,
1145
+ title: fields.title.trim(),
1146
+ body: normalizeBody(fields.body),
1147
+ labels: normalizeStringArray(fields.labels),
1148
+ assignees: normalizeStringArray(fields.assignees),
1149
+ milestone: normalizeMilestone(fields.milestone),
1150
+ reviewers: normalizeStringArray(fields.reviewers),
1151
+ isDraft: Boolean(fields.isDraft)
1152
+ };
1153
+ }
1154
+ function sameStringSet(left, right) {
1155
+ if (left.length !== right.length) return false;
1156
+ const sortedLeft = [...left].sort();
1157
+ const sortedRight = [...right].sort();
1158
+ return sortedLeft.every((value, index) => value === sortedRight[index]);
1159
+ }
1160
+ function diffStrings(source, target) {
1161
+ const targetSet = new Set(target);
1162
+ return source.filter((value) => !targetSet.has(value));
1163
+ }
1038
1164
  //#endregion
1039
1165
  //#region src/execute/sources/per-item.ts
1040
1166
  async function loadPerItemSource(storageDir) {
@@ -1074,67 +1200,20 @@ async function loadPerItemSource(storageDir) {
1074
1200
  };
1075
1201
  }
1076
1202
  function computePerItemOps(input) {
1077
- const ops = [];
1078
- const ifUnchangedSince = input.updatedAt;
1079
- if (input.current.title !== input.desired.title) ops.push({
1080
- action: "set-title",
1081
- number: input.number,
1082
- title: input.desired.title,
1083
- ifUnchangedSince
1084
- });
1085
- if (input.current.state !== input.desired.state) ops.push({
1086
- action: input.desired.state === "closed" ? "close" : "reopen",
1203
+ return computeExecuteDiffOps({
1087
1204
  number: input.number,
1088
- ifUnchangedSince
1089
- });
1090
- if (!sameStringSet(input.current.labels, input.desired.labels)) {
1091
- const additions = diffStrings(input.desired.labels, input.current.labels);
1092
- const deletions = diffStrings(input.current.labels, input.desired.labels);
1093
- if (additions.length > 0 && deletions.length > 0) ops.push({
1094
- action: "set-labels",
1095
- number: input.number,
1096
- labels: input.desired.labels,
1097
- ifUnchangedSince
1098
- });
1099
- else if (additions.length > 0) ops.push({
1100
- action: "add-labels",
1101
- number: input.number,
1102
- labels: additions,
1103
- ifUnchangedSince
1104
- });
1105
- else if (deletions.length > 0) ops.push({
1106
- action: "remove-labels",
1107
- number: input.number,
1108
- labels: deletions,
1109
- ifUnchangedSince
1110
- });
1111
- }
1112
- if (!sameStringSet(input.current.assignees, input.desired.assignees)) {
1113
- if (input.desired.assignees.length > 0) ops.push({
1114
- action: "set-assignees",
1115
- number: input.number,
1116
- assignees: input.desired.assignees,
1117
- ifUnchangedSince
1118
- });
1119
- else if (input.current.assignees.length > 0) ops.push({
1120
- action: "remove-assignees",
1121
- number: input.number,
1122
- assignees: input.current.assignees,
1123
- ifUnchangedSince
1124
- });
1125
- }
1126
- if (normalizeMilestone(input.current.milestone) !== normalizeMilestone(input.desired.milestone)) if (input.desired.milestone) ops.push({
1127
- action: "set-milestone",
1128
- number: input.number,
1129
- milestone: input.desired.milestone,
1130
- ifUnchangedSince
1131
- });
1132
- else ops.push({
1133
- action: "clear-milestone",
1134
- number: input.number,
1135
- ifUnchangedSince
1205
+ current: {
1206
+ ...input.current,
1207
+ body: null,
1208
+ reviewers: []
1209
+ },
1210
+ desired: {
1211
+ ...input.desired,
1212
+ body: null,
1213
+ reviewers: []
1214
+ },
1215
+ ifUnchangedSince: input.updatedAt
1136
1216
  });
1137
- return ops;
1138
1217
  }
1139
1218
  function parseFrontmatter(raw) {
1140
1219
  const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
@@ -1158,33 +1237,6 @@ function parseFrontmatter(raw) {
1158
1237
  milestone: normalizeMilestone(data.milestone)
1159
1238
  };
1160
1239
  }
1161
- function normalizeStringArray(value) {
1162
- if (!Array.isArray(value)) return [];
1163
- const unique = /* @__PURE__ */ new Set();
1164
- for (const entry of value) {
1165
- if (typeof entry !== "string") continue;
1166
- const normalized = entry.trim();
1167
- if (!normalized) continue;
1168
- unique.add(normalized);
1169
- }
1170
- return [...unique];
1171
- }
1172
- function sameStringSet(left, right) {
1173
- if (left.length !== right.length) return false;
1174
- const sortedLeft = [...left].sort();
1175
- const sortedRight = [...right].sort();
1176
- return sortedLeft.every((value, index) => value === sortedRight[index]);
1177
- }
1178
- function normalizeMilestone(value) {
1179
- if (typeof value !== "string") return null;
1180
- const normalized = value.trim();
1181
- return normalized.length > 0 ? normalized : null;
1182
- }
1183
- function diffStrings(source, target) {
1184
- const targetSet = new Set(target);
1185
- return source.filter((value) => !targetSet.has(value));
1186
- }
1187
-
1188
1240
  //#endregion
1189
1241
  //#region src/execute/sources/index.ts
1190
1242
  async function loadExecuteSources(executeFilePath) {
@@ -1199,10 +1251,32 @@ async function loadExecuteSources(executeFilePath) {
1199
1251
  ...executeMd.ops,
1200
1252
  ...perItem.ops
1201
1253
  ];
1254
+ const entries = mergedOps.map((op, mergedIndex) => {
1255
+ if (mergedIndex < ymlOps.length) return {
1256
+ op,
1257
+ source: "execute.yml",
1258
+ sourceIndex: mergedIndex,
1259
+ mergedIndex
1260
+ };
1261
+ const mdOffset = mergedIndex - ymlOps.length;
1262
+ if (mdOffset < executeMd.ops.length) return {
1263
+ op,
1264
+ source: "execute.md",
1265
+ sourceIndex: mdOffset,
1266
+ mergedIndex
1267
+ };
1268
+ return {
1269
+ op,
1270
+ source: "per-item",
1271
+ sourceIndex: mdOffset - executeMd.ops.length,
1272
+ mergedIndex
1273
+ };
1274
+ });
1202
1275
  const customErrors = validateExecuteRules(mergedOps);
1203
1276
  if (customErrors.length) throw new Error(`Invalid execute file: ${customErrors.join("; ")}`);
1204
1277
  return {
1205
1278
  ops: mergedOps,
1279
+ entries,
1206
1280
  warnings: [...executeMd.warnings, ...perItem.warnings],
1207
1281
  async writeRemaining(remainingIndexes) {
1208
1282
  await writeExecuteFile(executeFilePath, ymlOps.map((op, index) => ({
@@ -1215,12 +1289,11 @@ async function loadExecuteSources(executeFilePath) {
1215
1289
  if (!await pathExists(executeMdPath)) return;
1216
1290
  const mdOffset = ymlOps.length;
1217
1291
  const mdRemaining = /* @__PURE__ */ new Set();
1218
- for (const index of remainingIndexes) if (index >= mdOffset) mdRemaining.add(index - mdOffset);
1292
+ for (const index of remainingIndexes) if (index >= mdOffset && index < mdOffset + executeMd.ops.length) mdRemaining.add(index - mdOffset);
1219
1293
  await writeFile(executeMdPath, stringifyExecuteMd(executeMd, mdRemaining), "utf-8");
1220
1294
  }
1221
1295
  };
1222
1296
  }
1223
-
1224
1297
  //#endregion
1225
1298
  //#region src/execute/index.ts
1226
1299
  var ExecuteCancelledError = class extends Error {
@@ -1482,13 +1555,11 @@ function ensurePullAction(action, number, isPull) {
1482
1555
  function describeExecutionAction(action, number) {
1483
1556
  return `${action} #${number}`;
1484
1557
  }
1485
-
1486
1558
  //#endregion
1487
1559
  //#region src/sync/execution-log.ts
1488
1560
  async function appendExecutionResult(storageDirAbsolute, result) {
1489
1561
  await saveSyncState(storageDirAbsolute, appendExecution(await loadSyncState(storageDirAbsolute), result));
1490
1562
  }
1491
-
1492
1563
  //#endregion
1493
1564
  //#region src/utils/sync.ts
1494
1565
  function resolveSince(options, syncState) {
@@ -1500,7 +1571,6 @@ function normalizeIssueNumbers(numbers) {
1500
1571
  if (!numbers) return void 0;
1501
1572
  return [...new Set(numbers.filter((number) => Number.isInteger(number) && number > 0))];
1502
1573
  }
1503
-
1504
1574
  //#endregion
1505
1575
  //#region src/sync/markdown.ts
1506
1576
  const FIELDS_ALWAYS_KEEP = new Set(["labels", "assignees"]);
@@ -1635,7 +1705,6 @@ function getReactionEntries(reactions) {
1635
1705
  };
1636
1706
  }).filter((entry) => Boolean(entry));
1637
1707
  }
1638
-
1639
1708
  //#endregion
1640
1709
  //#region src/utils/string.ts
1641
1710
  function slugifyTitle(title, maxLength = 48) {
@@ -1643,7 +1712,6 @@ function slugifyTitle(title, maxLength = 48) {
1643
1712
  if (!normalized) return "item";
1644
1713
  return normalized.slice(0, maxLength).replace(/-+$/g, "") || "item";
1645
1714
  }
1646
-
1647
1715
  //#endregion
1648
1716
  //#region src/sync/paths.ts
1649
1717
  const FILE_NUMBER_PAD_LENGTH = 5;
@@ -1668,7 +1736,6 @@ function getItemFileName(number, title) {
1668
1736
  function getPrPatchPath(storageDirAbsolute, number, title) {
1669
1737
  return join(storageDirAbsolute, PULL_DIR_NAME, getItemFileName(number, title).replace(/\.md$/, ".patch"));
1670
1738
  }
1671
-
1672
1739
  //#endregion
1673
1740
  //#region src/sync/sync-repository-utils.ts
1674
1741
  function createCounters(scanned = 0, selected = 0) {
@@ -1711,7 +1778,6 @@ function relativeToStorage(storageDirAbsolute, absolutePath) {
1711
1778
  if (absolutePath.startsWith(storageDirAbsolute)) return absolutePath.slice(storageDirAbsolute.length + 1);
1712
1779
  return basename(absolutePath);
1713
1780
  }
1714
-
1715
1781
  //#endregion
1716
1782
  //#region src/sync/sync-repository-storage.ts
1717
1783
  async function resolveIssuePaths(storageDirAbsolute, kind, number, title, state, trackedFilePath) {
@@ -1838,7 +1904,6 @@ async function pruneMissingOpenTrackedItems(storageDirAbsolute, syncState, openN
1838
1904
  }
1839
1905
  return patchesDeleted;
1840
1906
  }
1841
-
1842
1907
  //#endregion
1843
1908
  //#region src/sync/sync-repository-item.ts
1844
1909
  async function prepareIssueCandidateSync(context, issue) {
@@ -2068,7 +2133,6 @@ async function resolveUniqueClosedTarget(closedDirAbsolute, fileName) {
2068
2133
  }
2069
2134
  return candidate;
2070
2135
  }
2071
-
2072
2136
  //#endregion
2073
2137
  //#region src/sync/sync-repository-provider.ts
2074
2138
  async function fetchIssueCandidatesByPagination(context, since) {
@@ -2098,7 +2162,6 @@ async function fetchIssueCandidatesByNumbers(context, numbers) {
2098
2162
  scanned: issues.length
2099
2163
  };
2100
2164
  }
2101
-
2102
2165
  //#endregion
2103
2166
  //#region src/utils/markdown.ts
2104
2167
  function getTimestamp(value) {
@@ -2124,7 +2187,6 @@ function escapeTableCell(value) {
2124
2187
  function escapeInlineCode(value) {
2125
2188
  return value.replace(/`/g, "\\`");
2126
2189
  }
2127
-
2128
2190
  //#endregion
2129
2191
  //#region src/sync/sync-repository-snapshot.ts
2130
2192
  async function writeRepoSnapshot(context) {
@@ -2227,7 +2289,6 @@ async function buildRepoSnapshot(context) {
2227
2289
  milestones
2228
2290
  };
2229
2291
  }
2230
-
2231
2292
  //#endregion
2232
2293
  //#region src/sync/sync-repository.ts
2233
2294
  async function syncRepository(options) {
@@ -2482,7 +2543,6 @@ function computeTotals(items) {
2482
2543
  trackedItems: totalIssues + totalPulls
2483
2544
  };
2484
2545
  }
2485
-
2486
2546
  //#endregion
2487
2547
  //#region src/cli/action-color.ts
2488
2548
  function colorizeAction(action, enabled = true) {
@@ -2536,7 +2596,6 @@ function wrapTextValue(value) {
2536
2596
  if (normalized.length <= 48) return normalized;
2537
2597
  return `${normalized.slice(0, 45)}...`;
2538
2598
  }
2539
-
2540
2599
  //#endregion
2541
2600
  //#region src/cli/errors.ts
2542
2601
  function withErrorHandling(fn) {
@@ -2547,7 +2606,6 @@ function withErrorHandling(fn) {
2547
2606
  });
2548
2607
  };
2549
2608
  }
2550
-
2551
2609
  //#endregion
2552
2610
  //#region src/cli/meta.ts
2553
2611
  const CLI_NAME = GHFS_NAME;
@@ -2565,7 +2623,6 @@ function ASCII_HEADER(repo) {
2565
2623
  function toGitHubRepoUrl(repo) {
2566
2624
  return `https://github.com/${repo}`;
2567
2625
  }
2568
-
2569
2626
  //#endregion
2570
2627
  //#region src/cli/printer.ts
2571
2628
  function createCliPrinter(command, options = {}) {
@@ -2686,7 +2743,7 @@ function createRichSyncReporter(printer) {
2686
2743
  printer.success(`Sync finished. ${event.summary.updatedIssues} issues and ${event.summary.updatedPulls} PRs updated${c.dim(` (${formatDuration(event.summary.durationMs)})`)}.`);
2687
2744
  },
2688
2745
  onError(event) {
2689
- const message = `Sync failed${event.stage && !isHiddenSyncStage(event.stage) ? ` while ${describeStage(event.stage)}` : ""}: ${toErrorMessage(event.error)}`;
2746
+ const message = `Sync failed${event.stage && !isHiddenSyncStage(event.stage) ? ` while ${describeStage(event.stage)}` : ""}: ${toErrorMessage$2(event.error)}`;
2690
2747
  if (hasSyncProgress) {
2691
2748
  syncProgress.error(message);
2692
2749
  hasSyncProgress = false;
@@ -2726,7 +2783,7 @@ function createPlainSyncReporter(printer, progressEvery) {
2726
2783
  },
2727
2784
  onError(event) {
2728
2785
  const stage = event.stage && !isHiddenSyncStage(event.stage) ? ` while ${describeStage(event.stage)}` : "";
2729
- printer.error(c.red(`Sync failed${stage}: ${toErrorMessage(event.error)}`));
2786
+ printer.error(c.red(`Sync failed${stage}: ${toErrorMessage$2(event.error)}`));
2730
2787
  }
2731
2788
  };
2732
2789
  }
@@ -2764,7 +2821,7 @@ function createRichExecuteReporter(printer) {
2764
2821
  printer.success(`${runMode} finished. Planned ${event.result.planned}, applied ${event.result.applied}, failed ${event.result.failed}.`);
2765
2822
  },
2766
2823
  onError(event) {
2767
- const message = `Execution failed: ${toErrorMessage(event.error)}`;
2824
+ const message = `Execution failed: ${toErrorMessage$2(event.error)}`;
2768
2825
  if (hasApplyProgress) {
2769
2826
  applyProgress.error(message);
2770
2827
  hasApplyProgress = false;
@@ -2788,7 +2845,7 @@ function createPlainExecuteReporter(printer) {
2788
2845
  printer.success(`${runMode} finished. Planned ${event.result.planned}, applied ${event.result.applied}, failed ${event.result.failed}.`);
2789
2846
  },
2790
2847
  onError(event) {
2791
- printer.error(c.red(`Execution failed: ${toErrorMessage(event.error)}`));
2848
+ printer.error(c.red(`Execution failed: ${toErrorMessage$2(event.error)}`));
2792
2849
  }
2793
2850
  };
2794
2851
  }
@@ -2808,7 +2865,7 @@ function formatStageCompletionLine(stage, snapshot, durationMs) {
2808
2865
  if (stage === "pagination") return `Pagination scanned ${countNoun(snapshot.scanned, "candidate item")}${duration}.`;
2809
2866
  if (stage === "fetch") return `Fetched updated issues/PRs (${snapshot.processed}/${snapshot.selected})${duration}.`;
2810
2867
  }
2811
- function toErrorMessage(error) {
2868
+ function toErrorMessage$2(error) {
2812
2869
  return error.message || String(error);
2813
2870
  }
2814
2871
  function formatKeyValueLines(entries, options = {}) {
@@ -2844,7 +2901,6 @@ function describeStage(stage) {
2844
2901
  if (stage === "prune") return "pruning local artifacts";
2845
2902
  return "saving sync state";
2846
2903
  }
2847
-
2848
2904
  //#endregion
2849
2905
  //#region src/cli/prompts.ts
2850
2906
  async function promptForToken() {
@@ -2913,11 +2969,10 @@ async function confirmExecuteApply(count) {
2913
2969
  }
2914
2970
  return result;
2915
2971
  }
2916
-
2917
2972
  //#endregion
2918
2973
  //#region src/cli/commands/execute.ts
2919
2974
  const PLAN_PREVIEW_LIMIT = 20;
2920
- const defaultDependencies = {
2975
+ const defaultDependencies$1 = {
2921
2976
  createCliPrinter,
2922
2977
  resolveConfig,
2923
2978
  isTTY: () => Boolean(process.stdin.isTTY),
@@ -2933,7 +2988,7 @@ const defaultDependencies = {
2933
2988
  function registerExecuteCommand(cli) {
2934
2989
  cli.command("execute", "Execute operations from .ghfs/execute.yml").option("--repo <repo>", "GitHub repository in owner/name format").option("--file <file>", "Path to execute yaml file").option("--run", "Run mutations on GitHub").option("--non-interactive", "Disable interactive prompts").option("--continue-on-error", "Continue applying ops after a failure").action(withErrorHandling(async (options) => runExecuteCommand(options)));
2935
2990
  }
2936
- async function runExecuteCommand(options, dependencies = defaultDependencies) {
2991
+ async function runExecuteCommand(options, dependencies = defaultDependencies$1) {
2937
2992
  const printer = dependencies.createCliPrinter("execute");
2938
2993
  const config = await dependencies.resolveConfig();
2939
2994
  const storageDirAbsolute = getStorageDirAbsolute(config);
@@ -3090,7 +3145,6 @@ function printExecutionSummary(printer, result) {
3090
3145
  }
3091
3146
  printer.success(summary);
3092
3147
  }
3093
-
3094
3148
  //#endregion
3095
3149
  //#region src/sync/status.ts
3096
3150
  async function getStatusSummary(config) {
@@ -3144,7 +3198,6 @@ async function getStatusSummary(config) {
3144
3198
  } : void 0
3145
3199
  };
3146
3200
  }
3147
-
3148
3201
  //#endregion
3149
3202
  //#region src/cli/commands/status.ts
3150
3203
  function registerStatusCommand(cli) {
@@ -3163,7 +3216,6 @@ function registerStatusCommand(cli) {
3163
3216
  printer.done("");
3164
3217
  }));
3165
3218
  }
3166
-
3167
3219
  //#endregion
3168
3220
  //#region src/cli/summary.ts
3169
3221
  function printSyncSummaryTable(printer, summary, title) {
@@ -3177,7 +3229,6 @@ function printSyncSummaryTable(printer, summary, title) {
3177
3229
  ["duration", formatDuration(summary.durationMs)]
3178
3230
  ]);
3179
3231
  }
3180
-
3181
3232
  //#endregion
3182
3233
  //#region src/cli/commands/sync.ts
3183
3234
  function registerSyncCommand(cli) {
@@ -3213,7 +3264,647 @@ function setupSyncCommand(command) {
3213
3264
  printer.done("Sync finished");
3214
3265
  }));
3215
3266
  }
3216
-
3267
+ //#endregion
3268
+ //#region src/cli/ui/server.ts
3269
+ const MIME_TYPES = {
3270
+ ".css": "text/css; charset=utf-8",
3271
+ ".html": "text/html; charset=utf-8",
3272
+ ".ico": "image/x-icon",
3273
+ ".js": "text/javascript; charset=utf-8",
3274
+ ".json": "application/json; charset=utf-8",
3275
+ ".map": "application/json; charset=utf-8",
3276
+ ".png": "image/png",
3277
+ ".svg": "image/svg+xml",
3278
+ ".txt": "text/plain; charset=utf-8",
3279
+ ".woff": "font/woff",
3280
+ ".woff2": "font/woff2"
3281
+ };
3282
+ async function createUiServer(options) {
3283
+ const uiRoot = resolve(options.uiDir);
3284
+ const uiRootPrefix = `${uiRoot}/`;
3285
+ const indexPath = resolve(uiRoot, "index.html");
3286
+ await assertFile(indexPath);
3287
+ const server = createServer(async (req, res) => {
3288
+ try {
3289
+ const pathname = normalizeRequestPath(req.url || "/");
3290
+ if (pathname === "/api/metadata.json") {
3291
+ const body = `${JSON.stringify(options.getMetadata())}\n`;
3292
+ res.statusCode = 200;
3293
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
3294
+ res.end(body);
3295
+ return;
3296
+ }
3297
+ const filePath = resolve(uiRoot, `.${pathname === "/" ? "/index.html" : pathname}`);
3298
+ const resolvedFilePath = (filePath === uiRoot || filePath.startsWith(uiRootPrefix)) && await isFile(filePath) ? filePath : indexPath;
3299
+ const body = await readFile(resolvedFilePath);
3300
+ res.statusCode = 200;
3301
+ res.setHeader("Content-Type", mimeTypeFor(resolvedFilePath));
3302
+ res.end(body);
3303
+ } catch (error) {
3304
+ res.statusCode = 500;
3305
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
3306
+ res.end(`Internal Server Error: ${toErrorMessage$1(error)}`);
3307
+ }
3308
+ });
3309
+ await new Promise((resolvePromise, reject) => {
3310
+ server.once("error", reject);
3311
+ server.listen(options.port, options.host, () => {
3312
+ resolvePromise();
3313
+ });
3314
+ });
3315
+ const address = server.address();
3316
+ if (!address || typeof address === "string") throw new Error("Failed to start UI HTTP server");
3317
+ return {
3318
+ server,
3319
+ close: () => {
3320
+ return new Promise((resolvePromise, reject) => {
3321
+ server.close((error) => {
3322
+ if (error) {
3323
+ reject(error);
3324
+ return;
3325
+ }
3326
+ resolvePromise();
3327
+ });
3328
+ });
3329
+ },
3330
+ url: `http://${address.address === "127.0.0.1" ? "localhost" : address.address}:${address.port}`
3331
+ };
3332
+ }
3333
+ function normalizeRequestPath(raw) {
3334
+ const pathOnly = raw.split("?")[0].split("#")[0] || "/";
3335
+ let decodedPath = pathOnly;
3336
+ try {
3337
+ decodedPath = decodeURIComponent(pathOnly);
3338
+ } catch {
3339
+ decodedPath = pathOnly;
3340
+ }
3341
+ const normalized = normalize(decodedPath);
3342
+ return normalized.startsWith("/") ? normalized : `/${normalized}`;
3343
+ }
3344
+ async function assertFile(path) {
3345
+ if (!await isFile(path)) throw new Error(`UI assets not found at ${path}`);
3346
+ }
3347
+ async function isFile(path) {
3348
+ try {
3349
+ return (await stat(path)).isFile();
3350
+ } catch {
3351
+ return false;
3352
+ }
3353
+ }
3354
+ function mimeTypeFor(path) {
3355
+ return MIME_TYPES[extname(path)] ?? "application/octet-stream";
3356
+ }
3357
+ function toErrorMessage$1(error) {
3358
+ if (error instanceof Error) return error.message;
3359
+ return String(error);
3360
+ }
3361
+ //#endregion
3362
+ //#region src/cli/ui/rpc.ts
3363
+ const REPLACEABLE_FAMILIES = new Set([
3364
+ "title",
3365
+ "body",
3366
+ "state",
3367
+ "labels",
3368
+ "assignees",
3369
+ "milestone",
3370
+ "reviewers",
3371
+ "draft"
3372
+ ]);
3373
+ function createServerFunctions(options) {
3374
+ const resolveRepoFn = options.resolveRepo ?? resolveRepo;
3375
+ const resolveAuthTokenFn = options.resolveAuthToken ?? resolveAuthToken;
3376
+ const executePendingChangesFn = options.executePendingChanges ?? executePendingChanges;
3377
+ const appendExecutionResultFn = options.appendExecutionResult ?? appendExecutionResult;
3378
+ const syncRepositoryFn = options.syncRepository ?? syncRepository;
3379
+ const loadExecuteSourcesFn = options.loadExecuteSources ?? loadExecuteSources;
3380
+ let executing = false;
3381
+ async function getBootstrap() {
3382
+ const syncState = await loadSyncState(options.storageDirAbsolute);
3383
+ const loaded = await loadExecuteSourcesFn(options.executeFilePath);
3384
+ const items = Object.values(syncState.items).map((entry) => {
3385
+ const item = entry.data.item;
3386
+ return {
3387
+ number: entry.number,
3388
+ kind: entry.kind,
3389
+ state: entry.state,
3390
+ title: item.title,
3391
+ updatedAt: item.updatedAt,
3392
+ createdAt: item.createdAt,
3393
+ closedAt: item.closedAt,
3394
+ author: item.author,
3395
+ url: item.url,
3396
+ labels: item.labels,
3397
+ assignees: item.assignees,
3398
+ milestone: item.milestone,
3399
+ commentsCount: entry.data.comments.length,
3400
+ isDraft: entry.data.pull?.isDraft,
3401
+ merged: entry.data.pull?.merged,
3402
+ requestedReviewers: entry.data.pull?.requestedReviewers ?? []
3403
+ };
3404
+ }).sort((left, right) => right.number - left.number);
3405
+ const queue = toQueueEntries(loaded.entries, syncState.repo);
3406
+ const openCount = items.filter((item) => item.state === "open").length;
3407
+ return {
3408
+ repo: syncState.repo,
3409
+ syncedAt: syncState.lastSyncedAt,
3410
+ lastSyncRunAt: syncState.lastSyncRun?.finishedAt,
3411
+ totalTracked: items.length,
3412
+ openCount,
3413
+ closedCount: items.length - openCount,
3414
+ warnings: loaded.warnings,
3415
+ items,
3416
+ queue,
3417
+ queueSummary: {
3418
+ total: queue.length,
3419
+ executeYml: queue.filter((entry) => entry.source === "execute.yml").length,
3420
+ executeMd: queue.filter((entry) => entry.source === "execute.md").length,
3421
+ perItem: queue.filter((entry) => entry.source === "per-item").length
3422
+ }
3423
+ };
3424
+ }
3425
+ async function getItemDetail(number) {
3426
+ const [syncState, repoSnapshot, loaded, yml] = await Promise.all([
3427
+ loadSyncState(options.storageDirAbsolute),
3428
+ readRepoSnapshot(options.storageDirAbsolute),
3429
+ loadExecuteSourcesFn(options.executeFilePath),
3430
+ readAndValidateExecuteFileWithSource(options.executeFilePath)
3431
+ ]);
3432
+ const tracked = syncState.items[String(number)];
3433
+ if (!tracked) throw new Error(`Item #${number} is not available in local mirror`);
3434
+ const queue = toQueueEntries(loaded.entries, syncState.repo).filter((entry) => entry.op.number === number);
3435
+ const effective = applyQueuedYmlOpsToItem(tracked, yml.ops.filter((op) => op.number === number));
3436
+ return {
3437
+ number: tracked.number,
3438
+ kind: tracked.kind,
3439
+ state: effective.state,
3440
+ title: effective.title,
3441
+ body: effective.body,
3442
+ updatedAt: tracked.data.item.updatedAt,
3443
+ createdAt: tracked.data.item.createdAt,
3444
+ closedAt: tracked.data.item.closedAt,
3445
+ author: tracked.data.item.author,
3446
+ url: tracked.data.item.url,
3447
+ labels: effective.labels,
3448
+ assignees: effective.assignees,
3449
+ milestone: effective.milestone,
3450
+ commentsCount: tracked.data.comments.length,
3451
+ comments: tracked.data.comments.map((comment) => ({
3452
+ id: comment.id,
3453
+ author: comment.author,
3454
+ body: comment.body || "",
3455
+ createdAt: comment.createdAt,
3456
+ updatedAt: comment.updatedAt
3457
+ })),
3458
+ isDraft: effective.isDraft,
3459
+ merged: tracked.data.pull?.merged,
3460
+ requestedReviewers: effective.reviewers,
3461
+ labelsCatalog: repoSnapshot?.labels ?? [],
3462
+ milestonesCatalog: (repoSnapshot?.milestones ?? []).map((m) => ({
3463
+ number: m.number,
3464
+ title: m.title,
3465
+ state: m.state
3466
+ })),
3467
+ queue
3468
+ };
3469
+ }
3470
+ async function queueItemEdits(payload) {
3471
+ const tracked = (await loadSyncState(options.storageDirAbsolute)).items[String(payload.number)];
3472
+ if (!tracked) throw new Error(`Item #${payload.number} is not available in local mirror`);
3473
+ const nextOps = toQueuedOps(tracked, payload);
3474
+ const yml = await readAndValidateExecuteFileWithSource(options.executeFilePath);
3475
+ const filteredOps = [];
3476
+ for (const [index, op] of yml.ops.entries()) {
3477
+ const family = getActionFamily(op.action);
3478
+ if (op.number === payload.number && REPLACEABLE_FAMILIES.has(family)) continue;
3479
+ filteredOps.push({
3480
+ ...op,
3481
+ _actionInput: yml.sourceActions[index] ?? op.action
3482
+ });
3483
+ }
3484
+ const writable = [...filteredOps.map(({ _actionInput, ...op }) => ({
3485
+ ...op,
3486
+ action: _actionInput
3487
+ })), ...nextOps.map((op) => ({
3488
+ ...op,
3489
+ action: op.action
3490
+ }))];
3491
+ await writeExecuteFile(options.executeFilePath, writable);
3492
+ return await notifyAndGetBootstrap(options.onStateChanged, getBootstrap);
3493
+ }
3494
+ async function removeQueueYmlEntry(index) {
3495
+ const yml = await readAndValidateExecuteFileWithSource(options.executeFilePath);
3496
+ if (!Number.isInteger(index) || index < 0 || index >= yml.ops.length) throw new Error(`Invalid execute.yml index: ${index}`);
3497
+ const writable = yml.ops.map((op, opIndex) => ({
3498
+ ...op,
3499
+ action: yml.sourceActions[opIndex] ?? op.action
3500
+ })).filter((_, opIndex) => opIndex !== index);
3501
+ await writeExecuteFile(options.executeFilePath, writable);
3502
+ return await notifyAndGetBootstrap(options.onStateChanged, getBootstrap);
3503
+ }
3504
+ async function refresh() {
3505
+ return await notifyAndGetBootstrap(options.onStateChanged, getBootstrap);
3506
+ }
3507
+ async function executeNow() {
3508
+ if (executing) throw new Error("Execution is already in progress");
3509
+ executing = true;
3510
+ try {
3511
+ const resolvedRepo = await resolveRepoFn({
3512
+ cwd: options.config.cwd,
3513
+ configRepo: options.config.repo,
3514
+ interactive: false
3515
+ });
3516
+ const token = await resolveAuthTokenFn({
3517
+ token: options.config.auth.token,
3518
+ interactive: false
3519
+ });
3520
+ const result = await executePendingChangesFn({
3521
+ config: options.config,
3522
+ repo: resolvedRepo.repo,
3523
+ token,
3524
+ executeFilePath: options.executeFilePath,
3525
+ apply: true,
3526
+ nonInteractive: true,
3527
+ continueOnError: false,
3528
+ reporter: {
3529
+ onStart: (event) => {
3530
+ fireAndForget(options.onExecuteProgress, {
3531
+ type: "start",
3532
+ planned: event.planned,
3533
+ repo: event.repo
3534
+ });
3535
+ },
3536
+ onProgress: (event) => {
3537
+ fireAndForget(options.onExecuteProgress, {
3538
+ type: "progress",
3539
+ repo: event.repo,
3540
+ planned: event.planned,
3541
+ completed: event.completed,
3542
+ applied: event.applied,
3543
+ failed: event.failed,
3544
+ detail: event.detail
3545
+ });
3546
+ },
3547
+ onError: (event) => {
3548
+ fireAndForget(options.onExecuteProgress, {
3549
+ type: "error",
3550
+ message: toErrorMessage(event.error)
3551
+ });
3552
+ }
3553
+ }
3554
+ });
3555
+ await appendExecutionResultFn(options.storageDirAbsolute, result);
3556
+ const affectedNumbers = [...new Set(result.details.filter((detail) => detail.status === "applied").map((detail) => detail.number))];
3557
+ if (affectedNumbers.length > 0) await syncRepositoryFn({
3558
+ config: options.config,
3559
+ repo: resolvedRepo.repo,
3560
+ token,
3561
+ numbers: affectedNumbers
3562
+ });
3563
+ await options.onExecuteComplete?.(result);
3564
+ return {
3565
+ result,
3566
+ bootstrap: await notifyAndGetBootstrap(options.onStateChanged, getBootstrap)
3567
+ };
3568
+ } finally {
3569
+ executing = false;
3570
+ }
3571
+ }
3572
+ return {
3573
+ getBootstrap,
3574
+ getItemDetail,
3575
+ queueItemEdits,
3576
+ removeQueueYmlEntry,
3577
+ refresh,
3578
+ executeNow
3579
+ };
3580
+ }
3581
+ function toQueueEntries(entries, repo) {
3582
+ return entries.map((entry) => ({
3583
+ id: `${entry.source}:${entry.sourceIndex}:${entry.mergedIndex}`,
3584
+ mergedIndex: entry.mergedIndex,
3585
+ source: entry.source,
3586
+ sourceIndex: entry.sourceIndex,
3587
+ editable: entry.source === "execute.yml",
3588
+ op: entry.op,
3589
+ description: describeCliOperation(entry.op, {
3590
+ tty: false,
3591
+ repo
3592
+ })
3593
+ }));
3594
+ }
3595
+ function toQueuedOps(tracked, payload) {
3596
+ const current = tracked.data.item;
3597
+ const reviewersCurrent = tracked.data.pull?.requestedReviewers ?? [];
3598
+ const desiredLabels = normalizeStringArray(payload.labels);
3599
+ const desiredAssignees = normalizeStringArray(payload.assignees);
3600
+ const desiredReviewers = normalizeStringArray(payload.reviewers);
3601
+ const desiredMilestone = normalizeMilestone(payload.milestone);
3602
+ const desiredTitle = payload.title.trim() || current.title;
3603
+ const desiredBody = payload.body.trim().length > 0 ? payload.body : current.body || "";
3604
+ const desiredState = payload.state;
3605
+ const desiredDraft = tracked.kind === "pull" ? Boolean(payload.isDraft) : tracked.data.pull?.isDraft;
3606
+ const ops = computeExecuteDiffOps({
3607
+ number: tracked.number,
3608
+ current: {
3609
+ title: current.title,
3610
+ body: current.body,
3611
+ state: current.state,
3612
+ labels: current.labels,
3613
+ assignees: current.assignees,
3614
+ milestone: current.milestone,
3615
+ reviewers: reviewersCurrent,
3616
+ isDraft: tracked.data.pull?.isDraft
3617
+ },
3618
+ desired: {
3619
+ title: desiredTitle,
3620
+ body: desiredBody,
3621
+ state: desiredState,
3622
+ labels: desiredLabels,
3623
+ assignees: desiredAssignees,
3624
+ milestone: desiredMilestone,
3625
+ reviewers: tracked.kind === "pull" ? desiredReviewers : reviewersCurrent,
3626
+ isDraft: desiredDraft
3627
+ },
3628
+ ifUnchangedSince: current.updatedAt,
3629
+ includeBody: true
3630
+ });
3631
+ const comment = payload.comment.trim();
3632
+ if (comment) ops.push({
3633
+ action: "add-comment",
3634
+ number: tracked.number,
3635
+ body: comment
3636
+ });
3637
+ return ops;
3638
+ }
3639
+ function applyQueuedYmlOpsToItem(tracked, ops) {
3640
+ let title = tracked.data.item.title;
3641
+ let body = tracked.data.item.body || "";
3642
+ let state = tracked.data.item.state;
3643
+ let labels = [...tracked.data.item.labels];
3644
+ let assignees = [...tracked.data.item.assignees];
3645
+ let milestone = tracked.data.item.milestone;
3646
+ let reviewers = [...tracked.data.pull?.requestedReviewers ?? []];
3647
+ let isDraft = tracked.data.pull?.isDraft;
3648
+ for (const op of ops) switch (op.action) {
3649
+ case "set-title":
3650
+ title = op.title;
3651
+ break;
3652
+ case "set-body":
3653
+ body = op.body;
3654
+ break;
3655
+ case "close":
3656
+ case "close-with-comment":
3657
+ state = "closed";
3658
+ break;
3659
+ case "reopen":
3660
+ state = "open";
3661
+ break;
3662
+ case "add-labels":
3663
+ labels = mergeStrings(labels, op.labels);
3664
+ break;
3665
+ case "remove-labels":
3666
+ labels = removeStrings(labels, op.labels);
3667
+ break;
3668
+ case "set-labels":
3669
+ labels = normalizeStringArray(op.labels);
3670
+ break;
3671
+ case "add-assignees":
3672
+ assignees = mergeStrings(assignees, op.assignees);
3673
+ break;
3674
+ case "remove-assignees":
3675
+ assignees = removeStrings(assignees, op.assignees);
3676
+ break;
3677
+ case "set-assignees":
3678
+ assignees = normalizeStringArray(op.assignees);
3679
+ break;
3680
+ case "set-milestone":
3681
+ milestone = String(op.milestone);
3682
+ break;
3683
+ case "clear-milestone":
3684
+ milestone = null;
3685
+ break;
3686
+ case "request-reviewers":
3687
+ reviewers = mergeStrings(reviewers, op.reviewers);
3688
+ break;
3689
+ case "remove-reviewers":
3690
+ reviewers = removeStrings(reviewers, op.reviewers);
3691
+ break;
3692
+ case "convert-to-draft":
3693
+ if (tracked.kind === "pull") isDraft = true;
3694
+ break;
3695
+ case "mark-ready-for-review":
3696
+ if (tracked.kind === "pull") isDraft = false;
3697
+ break;
3698
+ default: break;
3699
+ }
3700
+ return {
3701
+ title,
3702
+ body,
3703
+ state,
3704
+ labels,
3705
+ assignees,
3706
+ milestone,
3707
+ reviewers,
3708
+ isDraft
3709
+ };
3710
+ }
3711
+ async function notifyAndGetBootstrap(onStateChanged, getBootstrap) {
3712
+ const bootstrap = await getBootstrap();
3713
+ await onStateChanged?.(bootstrap);
3714
+ return bootstrap;
3715
+ }
3716
+ function getActionFamily(action) {
3717
+ switch (action) {
3718
+ case "set-title": return "title";
3719
+ case "set-body": return "body";
3720
+ case "close":
3721
+ case "reopen": return "state";
3722
+ case "add-labels":
3723
+ case "remove-labels":
3724
+ case "set-labels": return "labels";
3725
+ case "add-assignees":
3726
+ case "remove-assignees":
3727
+ case "set-assignees": return "assignees";
3728
+ case "set-milestone":
3729
+ case "clear-milestone": return "milestone";
3730
+ case "request-reviewers":
3731
+ case "remove-reviewers": return "reviewers";
3732
+ case "mark-ready-for-review":
3733
+ case "convert-to-draft": return "draft";
3734
+ case "add-comment":
3735
+ case "close-with-comment": return "comment";
3736
+ default: return "other";
3737
+ }
3738
+ }
3739
+ function mergeStrings(base, incoming) {
3740
+ const known = new Set(base);
3741
+ const merged = [...base];
3742
+ for (const value of normalizeStringArray(incoming)) {
3743
+ if (known.has(value)) continue;
3744
+ known.add(value);
3745
+ merged.push(value);
3746
+ }
3747
+ return merged;
3748
+ }
3749
+ function removeStrings(base, removing) {
3750
+ const removingSet = new Set(normalizeStringArray(removing));
3751
+ return base.filter((value) => !removingSet.has(value));
3752
+ }
3753
+ function fireAndForget(callback, payload) {
3754
+ if (!callback) return;
3755
+ Promise.resolve(callback(payload)).catch(() => {});
3756
+ }
3757
+ function toErrorMessage(error) {
3758
+ if (error instanceof Error) return error.message;
3759
+ return String(error);
3760
+ }
3761
+ async function readRepoSnapshot(storageDirAbsolute) {
3762
+ const path = resolve(storageDirAbsolute, REPO_SNAPSHOT_FILE_NAME);
3763
+ if (!await pathExists(path)) return void 0;
3764
+ try {
3765
+ const raw = await readFile(path, "utf8");
3766
+ return JSON.parse(raw);
3767
+ } catch {
3768
+ return;
3769
+ }
3770
+ }
3771
+ //#endregion
3772
+ //#region src/cli/ui/ws.ts
3773
+ async function createWsServer(options) {
3774
+ const wss = new WebSocketServer({
3775
+ host: options.host,
3776
+ port: options.port ?? 0
3777
+ });
3778
+ await waitForListen(wss);
3779
+ const wsClients = /* @__PURE__ */ new Set();
3780
+ let rpc;
3781
+ const serverFunctions = createServerFunctions({
3782
+ ...options,
3783
+ onStateChanged: (bootstrap) => rpc.broadcast.onStateChanged.asEvent(bootstrap),
3784
+ onExecuteProgress: (event) => rpc.broadcast.onExecuteProgress.asEvent(event),
3785
+ onExecuteComplete: (result) => rpc.broadcast.onExecuteComplete.asEvent(result)
3786
+ });
3787
+ rpc = createBirpcGroup(serverFunctions, [], {
3788
+ timeout: 12e4,
3789
+ onFunctionError(error, name) {
3790
+ console.error(c.red(`RPC error on "${name}":`));
3791
+ console.error(error);
3792
+ }
3793
+ });
3794
+ wss.on("connection", (ws) => {
3795
+ wsClients.add(ws);
3796
+ const channel = {
3797
+ post: (d) => ws.send(d),
3798
+ on: (fn) => {
3799
+ ws.on("message", (data) => {
3800
+ fn(data);
3801
+ });
3802
+ },
3803
+ serialize: stringify$1,
3804
+ deserialize: parse$1
3805
+ };
3806
+ rpc.updateChannels((channels) => {
3807
+ channels.push(channel);
3808
+ });
3809
+ ws.on("close", () => {
3810
+ wsClients.delete(ws);
3811
+ rpc.updateChannels((channels) => {
3812
+ const index = channels.indexOf(channel);
3813
+ if (index >= 0) channels.splice(index, 1);
3814
+ });
3815
+ });
3816
+ });
3817
+ return {
3818
+ wss,
3819
+ rpc,
3820
+ serverFunctions,
3821
+ close: async () => {
3822
+ for (const client of wsClients) client.terminate();
3823
+ await new Promise((resolve, reject) => {
3824
+ wss.close((error) => {
3825
+ if (error) {
3826
+ reject(error);
3827
+ return;
3828
+ }
3829
+ resolve();
3830
+ });
3831
+ });
3832
+ },
3833
+ getMetadata() {
3834
+ const address = wss.address();
3835
+ if (!address) throw new Error("WebSocket server is not listening");
3836
+ return {
3837
+ backend: "websocket",
3838
+ websocket: address.port
3839
+ };
3840
+ }
3841
+ };
3842
+ }
3843
+ async function waitForListen(server) {
3844
+ await new Promise((resolve, reject) => {
3845
+ server.once("listening", () => resolve());
3846
+ server.once("error", (error) => reject(error));
3847
+ });
3848
+ }
3849
+ //#endregion
3850
+ //#region src/cli/commands/ui.ts
3851
+ const defaultDependencies = {
3852
+ createCliPrinter,
3853
+ resolveConfig,
3854
+ ensureExecuteArtifacts,
3855
+ createUiServer,
3856
+ createWsServer
3857
+ };
3858
+ function registerUiCommand(cli) {
3859
+ cli.command("ui", "Serve local Web UI for synced mirror and execute queue").option("--host <host>", "Host for local UI server", { default: "127.0.0.1" }).option("--port <port>", "Port for local UI server", { default: 3589 }).action(withErrorHandling(async (options) => {
3860
+ await runUiCommand(options);
3861
+ }));
3862
+ }
3863
+ async function runUiCommand(options, dependencies = defaultDependencies) {
3864
+ const printer = dependencies.createCliPrinter("ui");
3865
+ const host = options.host || "127.0.0.1";
3866
+ const parsedPort = Number(options.port);
3867
+ const port = Number.isFinite(parsedPort) && parsedPort >= 0 ? parsedPort : 3589;
3868
+ const config = await dependencies.resolveConfig();
3869
+ const storageDirAbsolute = getStorageDirAbsolute(config);
3870
+ const executeFilePath = resolve(config.cwd, getExecuteFile(config));
3871
+ const uiDir = resolve(config.cwd, "dist/ui");
3872
+ await dependencies.ensureExecuteArtifacts(executeFilePath);
3873
+ const ws = await dependencies.createWsServer({
3874
+ host,
3875
+ config,
3876
+ executeFilePath,
3877
+ storageDirAbsolute
3878
+ });
3879
+ let http;
3880
+ try {
3881
+ http = await dependencies.createUiServer({
3882
+ host,
3883
+ port,
3884
+ uiDir,
3885
+ getMetadata: ws.getMetadata
3886
+ });
3887
+ } catch (error) {
3888
+ await ws.close().catch(() => {});
3889
+ throw error;
3890
+ }
3891
+ printer.success(`Web UI ready at ${http.url}`);
3892
+ printer.info(`RPC websocket listening on ${ws.getMetadata().websocket}`);
3893
+ printer.info("Press Ctrl+C to stop.");
3894
+ let shuttingDown = false;
3895
+ const shutdown = async () => {
3896
+ if (shuttingDown) return;
3897
+ shuttingDown = true;
3898
+ await Promise.allSettled([http.close(), ws.close()]);
3899
+ process.exit(0);
3900
+ };
3901
+ process.once("SIGINT", () => {
3902
+ shutdown();
3903
+ });
3904
+ process.once("SIGTERM", () => {
3905
+ shutdown();
3906
+ });
3907
+ }
3217
3908
  //#endregion
3218
3909
  //#region src/cli/index.ts
3219
3910
  function createCli() {
@@ -3221,6 +3912,7 @@ function createCli() {
3221
3912
  registerSyncCommand(cli);
3222
3913
  registerExecuteCommand(cli);
3223
3914
  registerStatusCommand(cli);
3915
+ registerUiCommand(cli);
3224
3916
  cli.help();
3225
3917
  cli.version(CLI_VERSION);
3226
3918
  return cli;
@@ -3228,10 +3920,8 @@ function createCli() {
3228
3920
  function runCli(argv = process.argv) {
3229
3921
  createCli().parse(argv);
3230
3922
  }
3231
-
3232
3923
  //#endregion
3233
3924
  //#region src/cli.ts
3234
3925
  runCli();
3235
-
3236
3926
  //#endregion
3237
- export { };
3927
+ export {};