@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 +843 -153
- package/dist/{factory-COZFMWsb.mjs → factory-DYHQBeCz.mjs} +1 -10
- package/dist/index.mjs +2 -4
- package/dist/ui/200.html +1 -0
- package/dist/ui/404.html +1 -0
- package/dist/ui/_nuxt/BR_H8Y5I.js +3 -0
- package/dist/ui/_nuxt/Cr8ZhsI1.js +3 -0
- package/dist/ui/_nuxt/D7-M_06k.js +1 -0
- package/dist/ui/_nuxt/DFs1tY5L.js +2 -0
- package/dist/ui/_nuxt/DSg5v247.js +1 -0
- package/dist/ui/_nuxt/_eJq5waD.js +1 -0
- package/dist/ui/_nuxt/builds/latest.json +1 -0
- package/dist/ui/_nuxt/builds/meta/560d1a3b-6050-40f3-af12-eecaef34ee9e.json +1 -0
- package/dist/ui/_nuxt/entry.CpgMGkX_.css +1 -0
- package/dist/ui/_nuxt/error-404.Cl81wDDB.css +1 -0
- package/dist/ui/_nuxt/error-500.eY3F7Rvm.css +1 -0
- package/dist/ui/index.html +1 -0
- package/package.json +38 -15
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-
|
|
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 ??
|
|
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
|
|
454
|
-
"#
|
|
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
|
-
"
|
|
462
|
-
"
|
|
463
|
-
"
|
|
464
|
-
"
|
|
465
|
-
"
|
|
466
|
-
"
|
|
467
|
-
"
|
|
468
|
-
"
|
|
469
|
-
"
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
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 {};
|