pm-github 2026.10.28 → 2026.10.29
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/CHANGELOG.md +6 -0
- package/README.md +37 -0
- package/dist/closeout.d.ts.map +1 -1
- package/dist/closeout.js +71 -30
- package/dist/closeout.js.map +1 -1
- package/dist/discussion-schema.d.ts +110 -0
- package/dist/discussion-schema.d.ts.map +1 -0
- package/dist/discussion-schema.js +41 -0
- package/dist/discussion-schema.js.map +1 -0
- package/dist/discussions.d.ts +26 -0
- package/dist/discussions.d.ts.map +1 -0
- package/dist/discussions.js +215 -0
- package/dist/discussions.js.map +1 -0
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +10 -4
- package/dist/gate.js.map +1 -1
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +2 -1
- package/dist/identity.js.map +1 -1
- package/dist/import-fidelity.d.ts +8 -3
- package/dist/import-fidelity.d.ts.map +1 -1
- package/dist/import-fidelity.js +39 -10
- package/dist/import-fidelity.js.map +1 -1
- package/dist/import-tags.d.ts +1 -1
- package/dist/import-tags.d.ts.map +1 -1
- package/dist/import-tags.js +8 -6
- package/dist/import-tags.js.map +1 -1
- package/dist/index.d.ts +48 -40
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +139 -60
- package/dist/index.js.map +1 -1
- package/dist/issue-forms.d.ts +1 -1
- package/dist/issue-forms.d.ts.map +1 -1
- package/dist/issue-forms.js +6 -4
- package/dist/issue-forms.js.map +1 -1
- package/dist/projects.d.ts +6 -4
- package/dist/projects.d.ts.map +1 -1
- package/dist/projects.js +21 -20
- package/dist/projects.js.map +1 -1
- package/dist/refresh.d.ts +2 -1
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +17 -8
- package/dist/refresh.js.map +1 -1
- package/dist/webhooks.d.ts.map +1 -1
- package/dist/webhooks.js +5 -2
- package/dist/webhooks.js.map +1 -1
- package/dist/workflow-diagnostic.d.ts +9 -0
- package/dist/workflow-diagnostic.d.ts.map +1 -0
- package/dist/workflow-diagnostic.js +62 -0
- package/dist/workflow-diagnostic.js.map +1 -0
- package/dist/workspace.d.ts +18 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +39 -0
- package/dist/workspace.js.map +1 -0
- package/docs/discussion-capture-example.json +14 -0
- package/docs/discussion-capture.md +44 -0
- package/manifest.json +1 -1
- package/package.json +8 -4
package/dist/index.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
import { readGithubWorkspace, canonicalGithubStatus, githubStatusKind, githubStatusIsTerminal } from "./workspace.js";
|
|
1
2
|
import { ownValue } from "./own-record.js";
|
|
2
3
|
import { uniqueGithubLabelNames } from "./label-names.js";
|
|
3
4
|
import { githubLabelsToTags, importTagArguments, importTagOptions, importTagValues, nativeTagAdditions } from "./import-tags.js";
|
|
4
5
|
export { githubLabelsToTags } from "./import-tags.js";
|
|
5
6
|
import { CloseoutUsageError, compactCloseoutReport, runCloseout } from "./closeout.js";
|
|
7
|
+
import { readWorkflowFailure } from "./workflow-diagnostic.js";
|
|
6
8
|
import { compileIssueForm, completeIssueForm, readRepositoryIssueForm, refuseIssueForm } from "./issue-forms.js";
|
|
7
9
|
export { compileIssueForm, completeIssueForm, readRepositoryIssueForm } from "./issue-forms.js";
|
|
8
|
-
import { AmbiguousCitationError, buildImportFidelity, fetchImportTimeline, importFidelityFields, matchImportedIssue, persistImportFidelity, readImportReleases, selectImportContextIndex, validateImportLifetime, } from "./import-fidelity.js";
|
|
10
|
+
import { AmbiguousCitationError, buildImportFidelity, fetchImportTimeline, importFidelityFields, matchImportedIssue, normalizeImportValue, persistImportFidelity, readImportReleases, selectImportContextIndex, serializeImportTags, validateImportLifetime, } from "./import-fidelity.js";
|
|
9
11
|
import { refreshChanges, runRefresh } from "./refresh.js";
|
|
10
12
|
import { exploreGithubIdentities, planGithubIdentityRepair, applyGithubIdentityRepair } from "./identity.js";
|
|
11
13
|
import { planGithubLabelMigration, readLabelMigrationJson } from "./labels.js";
|
|
14
|
+
import { captureGithubDiscussion, refuseDiscussion } from "./discussions.js";
|
|
15
|
+
export { captureGithubDiscussion, readGithubDiscussionComment, validateDiscussionCaptureReport } from "./discussions.js";
|
|
12
16
|
export { planGithubLabelMigration, readGithubLabelCatalog, validateGithubLabelMigrationReport } from "./labels.js";
|
|
13
17
|
export { exploreGithubIdentities, planGithubIdentityRepair, applyGithubIdentityRepair } from "./identity.js";
|
|
14
18
|
import { acceptGithubWebhook, reconcileGithubWebhooks, WebhookRefusal } from "./webhooks.js";
|
|
@@ -329,7 +333,7 @@ async function request(method, url, token, payload, maxRetries = 4) {
|
|
|
329
333
|
attempt++;
|
|
330
334
|
continue;
|
|
331
335
|
}
|
|
332
|
-
throw new Error(`GitHub API returned HTTP ${res.status}`);
|
|
336
|
+
throw new Error(`GitHub API returned HTTP ${res.status} for ${method} ${new URL(url).pathname}`);
|
|
333
337
|
}
|
|
334
338
|
}
|
|
335
339
|
/** Fetch a single GitHub REST endpoint via GET and return the decoded
|
|
@@ -958,10 +962,11 @@ export function deriveAtomicItemId(repo, issueNumber, idPrefix, normalizeItemId)
|
|
|
958
962
|
return normalizeItemId(`github-${repoToken}-${issueNumber}`, idPrefix);
|
|
959
963
|
}
|
|
960
964
|
/** Map one rendered import entry to its reversible SDK mutation sequence. */
|
|
961
|
-
export function buildAtomicImportMutations(repo, entry, idPrefix, normalizeItemId, updateOnly = false) {
|
|
965
|
+
export function buildAtomicImportMutations(repo, entry, idPrefix, normalizeItemId, updateOnly = false, statuses) {
|
|
962
966
|
if (updateOnly && !entry.match?.id)
|
|
963
967
|
throw new CommandError("Refresh refuses an unlinked target; no item may be created.");
|
|
964
|
-
const
|
|
968
|
+
const closing = canonicalGithubStatus(entry.status, statuses) === canonicalGithubStatus("closed", statuses);
|
|
969
|
+
const changed = updateOnly ? new Set(refreshChanges(entry, statuses).map(change => change.field)) : undefined;
|
|
965
970
|
const audit = updateOnly ? { comment: [`Refreshed linked GitHub issue ${repo.toLowerCase()}#${entry.issueNumber}`] } : {};
|
|
966
971
|
if (entry.linkOnly && entry.match?.id) {
|
|
967
972
|
return { itemId: entry.match.id, mutations: [{ op: "update", id: entry.match.id, options: { ...(!changed || changed.has("tags") ? importTagOptions(entry) : {}), ...audit } }] };
|
|
@@ -974,13 +979,13 @@ export function buildAtomicImportMutations(repo, entry, idPrefix, normalizeItemI
|
|
|
974
979
|
...importTagOptions(entry),
|
|
975
980
|
...(entry.assignee ? { assignee: entry.assignee } : {}),
|
|
976
981
|
...(entry.milestone ? { sprint: entry.milestone } : {}),
|
|
977
|
-
...(entry.status
|
|
982
|
+
...((!closing && githubStatusIsTerminal(entry.status, statuses)) ? { closeReason: entry.fidelity?.github_close_reason ?? `GitHub issue #${entry.issueNumber} closed` } : {}),
|
|
978
983
|
};
|
|
979
984
|
// Explicit terminal reconciliation also handles canceled -> completed. The
|
|
980
985
|
// supported force option permits that source-authoritative repeat closure;
|
|
981
986
|
// finalization restores the real dates before the transaction commits.
|
|
982
|
-
const statusOptions =
|
|
983
|
-
? entry.fidelity ? { status:
|
|
987
|
+
const statusOptions = closing
|
|
988
|
+
? entry.fidelity ? { status: entry.status, closeReason: entry.fidelity.github_close_reason, force: true } : {}
|
|
984
989
|
: { status: entry.status };
|
|
985
990
|
const managedItemId = deriveAtomicItemId(repo, entry.issueNumber, idPrefix, normalizeItemId);
|
|
986
991
|
// A missing match and a match at our deterministic external-key id use the
|
|
@@ -990,7 +995,7 @@ export function buildAtomicImportMutations(repo, entry, idPrefix, normalizeItemI
|
|
|
990
995
|
// create step treats an existing stable id as already applied; update then
|
|
991
996
|
// makes later content-bearing transactions refresh the item normally.
|
|
992
997
|
if (!updateOnly && (!entry.match?.id || entry.match.id === managedItemId)) {
|
|
993
|
-
const createStatus =
|
|
998
|
+
const createStatus = closing ? "open" : entry.status;
|
|
994
999
|
const mutations = [{
|
|
995
1000
|
op: "create",
|
|
996
1001
|
id: managedItemId,
|
|
@@ -1003,7 +1008,7 @@ export function buildAtomicImportMutations(repo, entry, idPrefix, normalizeItemI
|
|
|
1003
1008
|
...statusOptions,
|
|
1004
1009
|
},
|
|
1005
1010
|
}];
|
|
1006
|
-
if (
|
|
1011
|
+
if (closing && !entry.fidelity) {
|
|
1007
1012
|
mutations.push({
|
|
1008
1013
|
op: "close",
|
|
1009
1014
|
id: managedItemId,
|
|
@@ -1030,7 +1035,7 @@ export function buildAtomicImportMutations(repo, entry, idPrefix, normalizeItemI
|
|
|
1030
1035
|
id: itemId,
|
|
1031
1036
|
options: updateOptions,
|
|
1032
1037
|
}];
|
|
1033
|
-
if (
|
|
1038
|
+
if (closing && !entry.fidelity) {
|
|
1034
1039
|
mutations.push({
|
|
1035
1040
|
op: "close",
|
|
1036
1041
|
id: itemId,
|
|
@@ -1046,13 +1051,13 @@ export function buildAtomicImportMutations(repo, entry, idPrefix, normalizeItemI
|
|
|
1046
1051
|
* The importer's desired-state and compensation writes leave retries stable;
|
|
1047
1052
|
* local edits and later state cycles invalidate completed journals.
|
|
1048
1053
|
*/
|
|
1049
|
-
async function importContextVersions(pmRoot, entries) {
|
|
1054
|
+
async function importContextVersions(pmRoot, entries, statuses) {
|
|
1050
1055
|
const versions = [];
|
|
1051
1056
|
for (const entry of entries) {
|
|
1052
1057
|
if (!entry.fidelity || !entry.match?.id)
|
|
1053
1058
|
continue;
|
|
1054
1059
|
const snapshot = await sdkReadHistorySnapshot(sdkGetHistoryPath(pmRoot, entry.match.id), entry.match.id);
|
|
1055
|
-
const context = snapshot.entries[selectImportContextIndex(snapshot.entries, entry.status)];
|
|
1060
|
+
const context = snapshot.entries[selectImportContextIndex(snapshot.entries, entry.status, statuses)];
|
|
1056
1061
|
if (context)
|
|
1057
1062
|
versions.push(`${entry.match.id}:${crypto.createHash("sha256").update(JSON.stringify(context)).digest("hex")}`);
|
|
1058
1063
|
}
|
|
@@ -1197,6 +1202,7 @@ async function commitGithubImportTransaction(config) {
|
|
|
1197
1202
|
export async function importGithubAtomic(pmRoot, repo, entries, opts = {}) {
|
|
1198
1203
|
pmRoot = sdkResolvePmRoot(process.cwd(), pmRoot);
|
|
1199
1204
|
const { commitItemMutations: commit, normalizeItemId, readSettings, } = await resolveAtomicSdkFunctions(opts);
|
|
1205
|
+
const workspace = await readGithubWorkspace(pmRoot);
|
|
1200
1206
|
let idPrefix = "pm-";
|
|
1201
1207
|
try {
|
|
1202
1208
|
const settings = await readSettings(pmRoot);
|
|
@@ -1213,11 +1219,11 @@ export async function importGithubAtomic(pmRoot, repo, entries, opts = {}) {
|
|
|
1213
1219
|
// by the stable GitHub issue number so a retry whose API page/order changed
|
|
1214
1220
|
// supplies the exact same plan as well as the same transaction id.
|
|
1215
1221
|
for (const entry of [...entries].sort((a, b) => a.issueNumber - b.issueNumber)) {
|
|
1216
|
-
const planned = buildAtomicImportMutations(repo, entry, idPrefix, normalizeItemId, opts.updateOnly);
|
|
1222
|
+
const planned = buildAtomicImportMutations(repo, entry, idPrefix, normalizeItemId, opts.updateOnly, workspace.statuses);
|
|
1217
1223
|
itemIds.set(entry.issueNumber, planned.itemId);
|
|
1218
1224
|
mutations.push(...planned.mutations);
|
|
1219
1225
|
}
|
|
1220
|
-
const transactionId = deriveAtomicTransactionId(repo, entries, mutations, await importContextVersions(pmRoot, entries));
|
|
1226
|
+
const transactionId = deriveAtomicTransactionId(repo, entries, mutations, await importContextVersions(pmRoot, entries, workspace.statuses));
|
|
1221
1227
|
// Update-only transactions derive their steps from the CURRENT item state,
|
|
1222
1228
|
// so a retry after a partial apply would fingerprint a different plan and
|
|
1223
1229
|
// orphan the journal behind a fresh transaction id. Persist the exact plan
|
|
@@ -2405,7 +2411,7 @@ export async function prepareGithubImport(issue, repo, opts, token, match, fetch
|
|
|
2405
2411
|
const timeline = await fidelityContext.timeline(issue, repo, token);
|
|
2406
2412
|
const fidelity = buildImportFidelity(issue, fidelityContext.releases, timeline);
|
|
2407
2413
|
const targetStatus = mapState(issue.state, issue.state_reason);
|
|
2408
|
-
const conflict = match?.status && match.status !== targetStatus && (
|
|
2414
|
+
const conflict = match?.status && canonicalGithubStatus(match.status, fidelityContext.statuses) !== canonicalGithubStatus(targetStatus, fidelityContext.statuses) && (githubStatusIsTerminal(match.status, fidelityContext.statuses) || issue.state === "closed")
|
|
2409
2415
|
? { local: match.status, github: issue.state, rule: identity.linkOnly ? "preserve-provider-owner" : "github-source-wins" } : undefined;
|
|
2410
2416
|
const kind = issue.pull_request ? "PR" : "issue";
|
|
2411
2417
|
const labels = issue.labels.map((label) => label.name).filter(Boolean);
|
|
@@ -2451,6 +2457,16 @@ export async function prepareGithubImport(issue, repo, opts, token, match, fetch
|
|
|
2451
2457
|
match,
|
|
2452
2458
|
};
|
|
2453
2459
|
}
|
|
2460
|
+
/** Recognize a complete source projection without treating the SDK mutation clock as content. */
|
|
2461
|
+
export function importEntryUnchanged(entry, types, statuses) {
|
|
2462
|
+
if (!entry.match?.id || !entry.fidelity || refreshChanges(entry, statuses).length !== 0)
|
|
2463
|
+
return false;
|
|
2464
|
+
if (entry.linkOnly)
|
|
2465
|
+
return true;
|
|
2466
|
+
const fields = { type: entry.itemType, description: entry.description, body: entry.body,
|
|
2467
|
+
...(entry.assignee ? { assignee: entry.assignee } : {}), ...(entry.milestone ? { sprint: entry.milestone } : {}) };
|
|
2468
|
+
return Object.entries(fields).every(([field, value]) => normalizeImportValue(field, ownValue(entry.match, field) ?? "", types, statuses) === normalizeImportValue(field, value, types, statuses));
|
|
2469
|
+
}
|
|
2454
2470
|
/**
|
|
2455
2471
|
* Verify that a gated import's plan accounts for every fetched issue exactly once.
|
|
2456
2472
|
*
|
|
@@ -2622,11 +2638,11 @@ function gateImportWrites(pmRoot) {
|
|
|
2622
2638
|
* throws {@link CommandError} (with a semantic exit code) on failure.
|
|
2623
2639
|
*
|
|
2624
2640
|
* @param repoArg - The `owner/repo` to import from.
|
|
2625
|
-
* @param
|
|
2641
|
+
* @param pmPath - Workspace root or pm data dir.
|
|
2626
2642
|
* @param opts - Normalized import options.
|
|
2627
2643
|
* @param deps - Injectable collaborators (token resolver, fetchers, etc.) for tests.
|
|
2628
2644
|
*/
|
|
2629
|
-
export async function runImport(repoArg,
|
|
2645
|
+
export async function runImport(repoArg, pmPath, opts, dependencies = {}) {
|
|
2630
2646
|
if (!repoArg || !repoArg.includes("/")) {
|
|
2631
2647
|
throw new CommandError("Usage: pm github import <owner/repo> [--all|--state open|closed|all] " +
|
|
2632
2648
|
"[--labels bug,enhancement] [--since <iso>] [--assignee <login>] " +
|
|
@@ -2668,7 +2684,7 @@ export async function runImport(repoArg, pmRoot, opts, dependencies = {}) {
|
|
|
2668
2684
|
console.error("No issues found.");
|
|
2669
2685
|
// Even a no-op gated import must prove the workspace is gateable (a Git work
|
|
2670
2686
|
// tree the scanner can read), so the empty-change gate scan runs here too.
|
|
2671
|
-
const emptyGateReceipt = opts.gate && !opts.dryRun ? gateImportWrites(
|
|
2687
|
+
const emptyGateReceipt = opts.gate && !opts.dryRun ? gateImportWrites(pmPath) : undefined;
|
|
2672
2688
|
if (opts.dryRun && (opts.atomic || missingIssues.length > 0)) {
|
|
2673
2689
|
return {
|
|
2674
2690
|
dryRun: true,
|
|
@@ -2688,6 +2704,8 @@ export async function runImport(repoArg, pmRoot, opts, dependencies = {}) {
|
|
|
2688
2704
|
// reported "would import N, skip 0" where the real run performs updates for
|
|
2689
2705
|
// already-linked issues. A preview that overstates creates reads as "this will
|
|
2690
2706
|
// duplicate my whole tracker" and is the one thing --dry-run exists to rule out.
|
|
2707
|
+
const workspace = await readGithubWorkspace(pmPath);
|
|
2708
|
+
const pmRoot = workspace.pmRoot;
|
|
2691
2709
|
/** Read the complete raw corpus in-process, retaining all provider metadata and bodies. */
|
|
2692
2710
|
const readImportItems = async () => {
|
|
2693
2711
|
if (dependencies.readItems)
|
|
@@ -2697,7 +2715,7 @@ export async function runImport(repoArg, pmRoot, opts, dependencies = {}) {
|
|
|
2697
2715
|
};
|
|
2698
2716
|
const existingItems = await readImportItems();
|
|
2699
2717
|
const existing = indexByProvenance(existingItems);
|
|
2700
|
-
const fidelityContext = { items: existingItems, releases: readImportReleases(pmRoot),
|
|
2718
|
+
const fidelityContext = { statuses: workspace.statuses, items: existingItems, releases: readImportReleases(pmRoot),
|
|
2701
2719
|
timeline: dependencies.fetchIssueTimeline ?? (dependencies.fetchIssues ? async () => [] : fetchImportTimeline) };
|
|
2702
2720
|
let imported = 0;
|
|
2703
2721
|
let updated = 0;
|
|
@@ -2841,7 +2859,14 @@ export async function runImport(repoArg, pmRoot, opts, dependencies = {}) {
|
|
|
2841
2859
|
...fetchFields,
|
|
2842
2860
|
};
|
|
2843
2861
|
}
|
|
2844
|
-
|
|
2862
|
+
// Mixed create/update batches can select a fresh journal after the first
|
|
2863
|
+
// apply. Replaying an otherwise identical SDK UPDATE advances updated_at.
|
|
2864
|
+
// Never hide an interrupted journal behind the unchanged-state shortcut.
|
|
2865
|
+
const typeNames = new Map(Object.entries(workspace.types.alias_to_type));
|
|
2866
|
+
const unchanged = prepared.every(entry => importEntryUnchanged(entry, typeNames, workspace.statuses)) && (await listUnfinishedGithubImports(pmRoot)).length === 0;
|
|
2867
|
+
const result = unchanged ? { transactionId: undefined, recovered: false, recoveredItems: undefined, imported: 0, updated: 0,
|
|
2868
|
+
itemIds: new Map(prepared.map(entry => [entry.issueNumber, entry.match.id])) }
|
|
2869
|
+
: await (dependencies.commitAtomic ?? importGithubAtomic)(pmRoot, repo, prepared);
|
|
2845
2870
|
if (opts.gate)
|
|
2846
2871
|
verifyImportedProvenance(await readImportItems(), prepared, repo);
|
|
2847
2872
|
// Planned native comments are part of the verified plan: a contended lock,
|
|
@@ -2861,7 +2886,9 @@ export async function runImport(repoArg, pmRoot, opts, dependencies = {}) {
|
|
|
2861
2886
|
}
|
|
2862
2887
|
}
|
|
2863
2888
|
failClosedOnPlanDivergence(opts.gate, skipped, skipped, commentWriteFailures);
|
|
2864
|
-
if (
|
|
2889
|
+
if (unchanged)
|
|
2890
|
+
console.error("Atomic import unchanged; no item transaction was needed.");
|
|
2891
|
+
else if (result.recovered) {
|
|
2865
2892
|
console.error(`Atomic import recovered transaction ${result.transactionId} covering ${result.recoveredItems ?? prepared.length} item(s).`);
|
|
2866
2893
|
}
|
|
2867
2894
|
else {
|
|
@@ -2878,6 +2905,7 @@ export async function runImport(repoArg, pmRoot, opts, dependencies = {}) {
|
|
|
2878
2905
|
const gateReceipt = opts.gate ? gateImportWrites(pmRoot) : undefined;
|
|
2879
2906
|
return {
|
|
2880
2907
|
transactionId: result.transactionId,
|
|
2908
|
+
...(unchanged ? { unchanged: true } : {}),
|
|
2881
2909
|
recovered: result.recovered,
|
|
2882
2910
|
imported: result.imported,
|
|
2883
2911
|
updated: result.updated - (result.recovered ? 0 : adoptionCount),
|
|
@@ -2962,22 +2990,22 @@ export async function runImport(repoArg, pmRoot, opts, dependencies = {}) {
|
|
|
2962
2990
|
continue;
|
|
2963
2991
|
}
|
|
2964
2992
|
// Reconcile status separately.
|
|
2965
|
-
if (status === "canceled" && match.status !== "canceled") {
|
|
2993
|
+
if (canonicalGithubStatus(status, workspace.statuses) === canonicalGithubStatus("canceled", workspace.statuses) && canonicalGithubStatus(match.status, workspace.statuses) !== canonicalGithubStatus("canceled", workspace.statuses)) {
|
|
2966
2994
|
const cancel = pmRun(["--path", pmRoot, "update", match.id, "--status", "canceled", "--close-reason", prepared.fidelity.github_close_reason]);
|
|
2967
2995
|
if (!cancel.ok)
|
|
2968
2996
|
throw new CommandError(`Could not cancel GitHub issue #${issue.number}.`);
|
|
2969
2997
|
}
|
|
2970
|
-
if (status === "closed" && match.status !== "closed") {
|
|
2998
|
+
if (canonicalGithubStatus(status, workspace.statuses) === canonicalGithubStatus("closed", workspace.statuses) && canonicalGithubStatus(match.status, workspace.statuses) !== canonicalGithubStatus("closed", workspace.statuses)) {
|
|
2971
2999
|
// Close through `pm close` so the reason is real provenance; pass the
|
|
2972
3000
|
// source completion time as --completed-at when GitHub recorded one.
|
|
2973
|
-
const close = pmRun([...githubCloseArgs(match.id), ...(match.status
|
|
3001
|
+
const close = pmRun([...githubCloseArgs(match.id), ...(githubStatusIsTerminal(match.status, workspace.statuses) ? ["--force"] : [])]);
|
|
2974
3002
|
if (!close.ok) {
|
|
2975
3003
|
console.error(`#${issue.number}: close reconciliation failed — ${close.stderr}`);
|
|
2976
3004
|
skipped++;
|
|
2977
3005
|
continue;
|
|
2978
3006
|
}
|
|
2979
3007
|
}
|
|
2980
|
-
else if (status === "open" && (match.status
|
|
3008
|
+
else if (canonicalGithubStatus(status, workspace.statuses) === canonicalGithubStatus("open", workspace.statuses) && githubStatusIsTerminal(match.status, workspace.statuses)) {
|
|
2981
3009
|
const reopen = pmRun(["--path", pmRoot, "update", match.id, "--status", "open", "--message", `GitHub issue #${issue.number} reopened`]);
|
|
2982
3010
|
if (!reopen.ok) {
|
|
2983
3011
|
console.error(`#${issue.number}: reopen reconciliation failed — ${reopen.stderr}`);
|
|
@@ -2999,8 +3027,8 @@ export async function runImport(repoArg, pmRoot, opts, dependencies = {}) {
|
|
|
2999
3027
|
// `require_close_reason` rejects `pm create --status closed`. Create the
|
|
3000
3028
|
// item open, then close it through `pm close` with the source's real
|
|
3001
3029
|
// completion timestamp (`closed_at`) as --completed-at provenance.
|
|
3002
|
-
const createStatus = status === "closed" ? "open" : status;
|
|
3003
|
-
const mustClose = status === "closed";
|
|
3030
|
+
const createStatus = canonicalGithubStatus(status, workspace.statuses) === canonicalGithubStatus("closed", workspace.statuses) ? "open" : status;
|
|
3031
|
+
const mustClose = canonicalGithubStatus(status, workspace.statuses) === canonicalGithubStatus("closed", workspace.statuses);
|
|
3004
3032
|
const createArgs = [
|
|
3005
3033
|
"--path", pmRoot, "create",
|
|
3006
3034
|
"--title", title,
|
|
@@ -3011,7 +3039,7 @@ export async function runImport(repoArg, pmRoot, opts, dependencies = {}) {
|
|
|
3011
3039
|
"--tags", importTagOptions(prepared).tags,
|
|
3012
3040
|
"--message", `Imported from GitHub #${issue.number}`,
|
|
3013
3041
|
];
|
|
3014
|
-
if (status === "canceled")
|
|
3042
|
+
if (canonicalGithubStatus(status, workspace.statuses) === canonicalGithubStatus("canceled", workspace.statuses))
|
|
3015
3043
|
createArgs.push("--close-reason", prepared.fidelity.github_close_reason);
|
|
3016
3044
|
if (assignee)
|
|
3017
3045
|
createArgs.push("--assignee", assignee);
|
|
@@ -3163,8 +3191,8 @@ function failClosedOnPlanDivergence(gate, plannedSkipped, actualSkipped, comment
|
|
|
3163
3191
|
// ---------------------------------------------------------------------------
|
|
3164
3192
|
// Sync core — push pm status changes back to GitHub (close / reopen)
|
|
3165
3193
|
// ---------------------------------------------------------------------------
|
|
3166
|
-
function pmStatusToGithubState(status) {
|
|
3167
|
-
return status
|
|
3194
|
+
function pmStatusToGithubState(status, statuses) {
|
|
3195
|
+
return githubStatusIsTerminal(status, statuses) ? "closed" : "open";
|
|
3168
3196
|
}
|
|
3169
3197
|
/**
|
|
3170
3198
|
* Require every requested item before a remote sync or export can begin.
|
|
@@ -3182,7 +3210,7 @@ function requireRequestedItems(items, requested) {
|
|
|
3182
3210
|
}
|
|
3183
3211
|
// Build the pm → GitHub issue sync plan: for each pm item linked to `repo`, emit
|
|
3184
3212
|
// a create-or-update entry keyed by the issue's provenance tag.
|
|
3185
|
-
export function planSync(items, repo) {
|
|
3213
|
+
export function planSync(items, repo, statuses) {
|
|
3186
3214
|
const plan = [];
|
|
3187
3215
|
const repoLc = repo.toLowerCase();
|
|
3188
3216
|
for (const item of items) {
|
|
@@ -3192,7 +3220,7 @@ export function planSync(items, repo) {
|
|
|
3192
3220
|
const p = parseProvenanceTag(tag);
|
|
3193
3221
|
if (!p || p.repo !== repoLc)
|
|
3194
3222
|
continue;
|
|
3195
|
-
const desired = pmStatusToGithubState(item.status);
|
|
3223
|
+
const desired = pmStatusToGithubState(item.status, statuses);
|
|
3196
3224
|
plan.push({
|
|
3197
3225
|
id: item.id,
|
|
3198
3226
|
number: p.number,
|
|
@@ -3227,7 +3255,8 @@ async function runSync(ctx) {
|
|
|
3227
3255
|
"(set GITHUB_TOKEN/GH_TOKEN or run `gh auth login`). Use --dry-run to preview without a token.", EXIT_CODE.USAGE);
|
|
3228
3256
|
}
|
|
3229
3257
|
const selected = requireRequestedItems(readPmItems(ctx.pm_root), scopedIds);
|
|
3230
|
-
const
|
|
3258
|
+
const workspace = await readGithubWorkspace(ctx.pm_root);
|
|
3259
|
+
const plan = planSync(selected, repo, workspace.statuses);
|
|
3231
3260
|
if (plan.length === 0) {
|
|
3232
3261
|
const scopeNote = scopedIds.length > 0 ? ` from --ids (${scopedIds.join(", ")})` : "";
|
|
3233
3262
|
console.error(`No pm items${scopeNote} linked to ${repo} ` +
|
|
@@ -3289,7 +3318,7 @@ async function runSync(ctx) {
|
|
|
3289
3318
|
}
|
|
3290
3319
|
// Convert a pm item into the GitHub issue create/update payload, dropping
|
|
3291
3320
|
// internal provenance tags from labels and applying the optional label map.
|
|
3292
|
-
function itemToGithubPayload(item, labelMap) {
|
|
3321
|
+
function itemToGithubPayload(item, labelMap, statuses) {
|
|
3293
3322
|
// Drop our internal provenance tags from exported labels, then apply any
|
|
3294
3323
|
// user-supplied label mapping (pm tag → GitHub label). Both issue provenance
|
|
3295
3324
|
// (`gh:owner/repo#N`) and project provenance (`gh-project:owner/number#itemId`)
|
|
@@ -3300,18 +3329,18 @@ function itemToGithubPayload(item, labelMap) {
|
|
|
3300
3329
|
title: item.title ?? "(untitled)",
|
|
3301
3330
|
body: item.body || item.description || "",
|
|
3302
3331
|
labels: applyLabelMap(labels, labelMap),
|
|
3303
|
-
state: item.status
|
|
3332
|
+
state: pmStatusToGithubState(item.status, statuses),
|
|
3304
3333
|
};
|
|
3305
3334
|
}
|
|
3306
3335
|
// Build the create/update plan. `repo` (lowercased) decides which provenance
|
|
3307
3336
|
// tags count as an "already exported to THIS repo" link → update; everything
|
|
3308
3337
|
// else is a create. Pure + side-effect free so it can be unit-tested and
|
|
3309
3338
|
// printed verbatim in --dry-run.
|
|
3310
|
-
export function buildExportPlan(items, repo, labelMap) {
|
|
3339
|
+
export function buildExportPlan(items, repo, labelMap, statuses) {
|
|
3311
3340
|
const repoLc = repo?.toLowerCase();
|
|
3312
3341
|
const plan = [];
|
|
3313
3342
|
for (const item of items) {
|
|
3314
|
-
const payload = itemToGithubPayload(item, labelMap);
|
|
3343
|
+
const payload = itemToGithubPayload(item, labelMap, statuses);
|
|
3315
3344
|
let number;
|
|
3316
3345
|
if (repoLc) {
|
|
3317
3346
|
for (const tag of item.tags ?? []) {
|
|
@@ -3473,7 +3502,8 @@ async function runExport(ctx) {
|
|
|
3473
3502
|
else if (["form-ref", "formRef", "form-answers", "formAnswers", "form-bindings", "formBindings"].some(flag => optionProvided(options, flag))) {
|
|
3474
3503
|
refuseIssueForm("invalid_form_input", [{ field: "form", reason: "selection", remedy: "Pass --form with --repo and --form-ref to enforce an issue-form contract." }]);
|
|
3475
3504
|
}
|
|
3476
|
-
const
|
|
3505
|
+
const workspace = await readGithubWorkspace(ctx.pm_root);
|
|
3506
|
+
const plan = buildExportPlan(selected, repo, labelMap, workspace.statuses);
|
|
3477
3507
|
for (const [index, completion] of completions.entries()) {
|
|
3478
3508
|
plan[index].payload.body = completion.body;
|
|
3479
3509
|
plan[index].payload.labels = uniqueGithubLabelNames([...plan[index].payload.labels, ...completion.report.contract.labels]);
|
|
@@ -3736,11 +3766,11 @@ function isGithubBlockerDependency(target) {
|
|
|
3736
3766
|
* @param items - The complete runtime-validated item corpus.
|
|
3737
3767
|
* @returns The candidate items, in corpus order.
|
|
3738
3768
|
*/
|
|
3739
|
-
export function selectBlockerCandidates(items) {
|
|
3769
|
+
export function selectBlockerCandidates(items, statuses) {
|
|
3740
3770
|
return items.filter((item) => {
|
|
3741
|
-
if (item.status === "blocked")
|
|
3771
|
+
if (githubStatusKind(item.status, statuses) === "blocked")
|
|
3742
3772
|
return true;
|
|
3743
|
-
if (
|
|
3773
|
+
if (githubStatusIsTerminal(item.status, statuses))
|
|
3744
3774
|
return false;
|
|
3745
3775
|
return (item.dependencies.some((dep) => dep.kind === "blocked_by" && isGithubBlockerDependency(dep.id))
|
|
3746
3776
|
|| (item.blockedBy !== undefined && isGithubBlockerDependency(item.blockedBy)));
|
|
@@ -4199,7 +4229,8 @@ async function runBlockers(ctx) {
|
|
|
4199
4229
|
}
|
|
4200
4230
|
const corpus = await readBlockerPmItems(ctx.pm_root);
|
|
4201
4231
|
const selected = requireRequestedItems(corpus, scopedIds);
|
|
4202
|
-
const
|
|
4232
|
+
const workspace = await readGithubWorkspace(ctx.pm_root);
|
|
4233
|
+
const candidates = selectBlockerCandidates(selected, workspace.statuses);
|
|
4203
4234
|
const perItem = candidates.map((item) => ({ item, references: collectBlockerReferences(item, fallbackRepo) }));
|
|
4204
4235
|
const unique = new Map();
|
|
4205
4236
|
for (const entry of perItem) {
|
|
@@ -4845,7 +4876,8 @@ async function runProjectImport(ctx) {
|
|
|
4845
4876
|
// Reading the local store is non-mutating and keeps dry-run faithful: linked
|
|
4846
4877
|
// project items must preview as updates, not misleading duplicate creates.
|
|
4847
4878
|
const pmItems = readPmItems(ctx.pm_root);
|
|
4848
|
-
const
|
|
4879
|
+
const workspace = await readGithubWorkspace(ctx.pm_root);
|
|
4880
|
+
const plan = buildProjectImportPlan(projectItems, ref, pmItems, statusMap, workspace.statuses);
|
|
4849
4881
|
let imported = 0;
|
|
4850
4882
|
let updated = 0;
|
|
4851
4883
|
let skipped = 0;
|
|
@@ -4878,9 +4910,12 @@ async function runProjectImport(ctx) {
|
|
|
4878
4910
|
// A `closed` mapping cannot go through `pm update --status closed`
|
|
4879
4911
|
// (governance require_close_reason rejects it since pm-cli 2026.8.3);
|
|
4880
4912
|
// the update omits --status and a separate `pm close` records the reason.
|
|
4881
|
-
const closeAfterUpdate = entry.mappedStatus ===
|
|
4882
|
-
if (entry.mappedStatus && !closeAfterUpdate)
|
|
4913
|
+
const closeAfterUpdate = entry.mappedStatus !== undefined && canonicalGithubStatus(entry.mappedStatus, workspace.statuses) === workspace.statuses.close_status;
|
|
4914
|
+
if (entry.mappedStatus && !closeAfterUpdate) {
|
|
4883
4915
|
updArgs.push("--status", entry.mappedStatus);
|
|
4916
|
+
if (githubStatusIsTerminal(entry.mappedStatus, workspace.statuses))
|
|
4917
|
+
updArgs.push("--close-reason", projectImportCloseReason(ref, entry.content));
|
|
4918
|
+
}
|
|
4884
4919
|
const upd = pmRun(updArgs);
|
|
4885
4920
|
if (!upd.ok) {
|
|
4886
4921
|
console.error(` ${entry.title}: update failed — ${upd.stderr}`);
|
|
@@ -4902,19 +4937,21 @@ async function runProjectImport(ctx) {
|
|
|
4902
4937
|
// `require_close_reason` rejects `pm create --status closed` (pm-cli
|
|
4903
4938
|
// 2026.8.3+). Create open, then close through `pm close` with factual
|
|
4904
4939
|
// provenance. `--json` is needed to read the assigned id for the close.
|
|
4905
|
-
const
|
|
4906
|
-
const
|
|
4940
|
+
const mustClose = canonicalGithubStatus(entry.status, workspace.statuses) === workspace.statuses.close_status;
|
|
4941
|
+
const createStatus = mustClose ? workspace.statuses.open_status : entry.status;
|
|
4907
4942
|
const createArgs = [
|
|
4908
4943
|
"--path", ctx.pm_root, "create",
|
|
4909
4944
|
"--title", entry.title,
|
|
4910
4945
|
"--type", itemType,
|
|
4911
4946
|
"--status", createStatus,
|
|
4912
4947
|
"--description", description,
|
|
4913
|
-
"--tags", entry.tags
|
|
4948
|
+
"--tags", serializeImportTags(entry.tags),
|
|
4914
4949
|
"--message", `Imported from GitHub project ${ref.owner}/${ref.number}`,
|
|
4915
4950
|
];
|
|
4916
4951
|
if (entry.body)
|
|
4917
4952
|
createArgs.push("--body", entry.body);
|
|
4953
|
+
if (!mustClose && githubStatusIsTerminal(entry.status, workspace.statuses))
|
|
4954
|
+
createArgs.push("--close-reason", projectImportCloseReason(ref, entry.content));
|
|
4918
4955
|
if (mustClose)
|
|
4919
4956
|
createArgs.push("--json");
|
|
4920
4957
|
const created = pmRun(createArgs);
|
|
@@ -5035,22 +5072,22 @@ async function applyPushEntry(entry, meta, ref, pmById, pmRoot, token) {
|
|
|
5035
5072
|
* @param pmRoot - Workspace root for the `pm` invocation.
|
|
5036
5073
|
* @returns The argv array.
|
|
5037
5074
|
*/
|
|
5038
|
-
export function buildPullEntryArgs(entry, pmRoot) {
|
|
5075
|
+
export function buildPullEntryArgs(entry, pmRoot, statuses) {
|
|
5039
5076
|
const reason = `GitHub project status → ${entry.toStatus}`;
|
|
5040
|
-
if (entry.toStatus === "closed") {
|
|
5077
|
+
if (canonicalGithubStatus(entry.toStatus, statuses) === (statuses?.close_status ?? "closed")) {
|
|
5041
5078
|
return ["--path", pmRoot, "close", entry.pmId, "--reason", reason];
|
|
5042
5079
|
}
|
|
5043
|
-
if (entry.toStatus
|
|
5080
|
+
if (githubStatusIsTerminal(entry.toStatus, statuses)) {
|
|
5044
5081
|
return [
|
|
5045
5082
|
"--path", pmRoot, "update", entry.pmId,
|
|
5046
|
-
"--status",
|
|
5083
|
+
"--status", canonicalGithubStatus(entry.toStatus, statuses),
|
|
5047
5084
|
"--close-reason", reason,
|
|
5048
5085
|
"--message", reason,
|
|
5049
5086
|
];
|
|
5050
5087
|
}
|
|
5051
5088
|
return [
|
|
5052
5089
|
"--path", pmRoot, "update", entry.pmId,
|
|
5053
|
-
"--status", entry.toStatus,
|
|
5090
|
+
"--status", canonicalGithubStatus(entry.toStatus, statuses),
|
|
5054
5091
|
"--message", reason,
|
|
5055
5092
|
];
|
|
5056
5093
|
}
|
|
@@ -5058,8 +5095,8 @@ export function buildPullEntryArgs(entry, pmRoot) {
|
|
|
5058
5095
|
// (records closed_at + close_reason); `canceled` keeps its distinct terminal
|
|
5059
5096
|
// state via `pm update` while recording `--close-reason`; everything else is a
|
|
5060
5097
|
// plain `pm update --status`.
|
|
5061
|
-
function applyPullEntry(entry, pmRoot) {
|
|
5062
|
-
const res = pmRun(buildPullEntryArgs(entry, pmRoot));
|
|
5098
|
+
function applyPullEntry(entry, pmRoot, statuses) {
|
|
5099
|
+
const res = pmRun(buildPullEntryArgs(entry, pmRoot, statuses));
|
|
5063
5100
|
return res.ok ? { changed: true } : { changed: false, error: res.stderr };
|
|
5064
5101
|
}
|
|
5065
5102
|
/**
|
|
@@ -5108,11 +5145,12 @@ async function runProjectSync(ctx) {
|
|
|
5108
5145
|
for (const it of scoped.selected)
|
|
5109
5146
|
if (it.id)
|
|
5110
5147
|
pmById.set(it.id, it);
|
|
5148
|
+
const workspace = await readGithubWorkspace(ctx.pm_root);
|
|
5111
5149
|
const pushPlan = doPush
|
|
5112
|
-
? buildProjectPushPlan(scoped.selected, ref, projectItems, meta.statusField, { addMissing, statusMap })
|
|
5150
|
+
? buildProjectPushPlan(scoped.selected, ref, projectItems, meta.statusField, { addMissing, statusMap, statuses: workspace.statuses })
|
|
5113
5151
|
: undefined;
|
|
5114
5152
|
const pullPlan = doPull
|
|
5115
|
-
? buildProjectPullPlan(scoped.selected, ref, projectItems, statusMap)
|
|
5153
|
+
? buildProjectPullPlan(scoped.selected, ref, projectItems, statusMap, workspace.statuses)
|
|
5116
5154
|
: undefined;
|
|
5117
5155
|
// Conflict resolution when applying BOTH directions: a linked item can appear
|
|
5118
5156
|
// in both plans. `--prefer pm` (default) lets push win (skip its pull entry);
|
|
@@ -5179,7 +5217,7 @@ async function runProjectSync(ctx) {
|
|
|
5179
5217
|
for (const e of pullActionable) {
|
|
5180
5218
|
if (prefer === "pm" && pushItemIds.has(e.itemId))
|
|
5181
5219
|
continue; // push wins
|
|
5182
|
-
const r = applyPullEntry(e, ctx.pm_root);
|
|
5220
|
+
const r = applyPullEntry(e, ctx.pm_root, workspace.statuses);
|
|
5183
5221
|
if (!r.changed) {
|
|
5184
5222
|
console.error(` pull ${e.pmId} "${e.title}": ${r.error}`);
|
|
5185
5223
|
pullFailed++;
|
|
@@ -5315,7 +5353,7 @@ const PROJECT_SYNC_FLAGS = [
|
|
|
5315
5353
|
const defineExtension = (module) => module;
|
|
5316
5354
|
export default defineExtension({
|
|
5317
5355
|
name: "pm-github",
|
|
5318
|
-
version: "2026.10.
|
|
5356
|
+
version: "2026.10.29",
|
|
5319
5357
|
activate(api) {
|
|
5320
5358
|
// -----------------------------------------------------------------------
|
|
5321
5359
|
// schema — declare the GitHub provenance fields so the workspace knows them
|
|
@@ -5575,6 +5613,30 @@ export default defineExtension({
|
|
|
5575
5613
|
});
|
|
5576
5614
|
},
|
|
5577
5615
|
});
|
|
5616
|
+
api.registerCommand({
|
|
5617
|
+
name: "github discussion capture",
|
|
5618
|
+
description: "Preview explicitly marked issue-comment spans as native Decisions. Apply only the reviewed digest; source instructions remain quoted data.",
|
|
5619
|
+
flags: [
|
|
5620
|
+
{ long: "--input", value_name: "file", description: "Strict JSON selection: repo, issue, item, selections with comment/start/end/quote/marked:true" },
|
|
5621
|
+
{ long: "--approve", value_name: "digest", description: "Apply the exact reviewed preview, re-reading every source and native target" },
|
|
5622
|
+
{ long: "--max-output-bytes", value_name: "N", description: "Complete JSON receipt budget, integer 1024..131072 (default 65536)" },
|
|
5623
|
+
],
|
|
5624
|
+
async run(ctx) {
|
|
5625
|
+
const { input, approve, maxOutputBytes } = ctx.options;
|
|
5626
|
+
if (typeof input !== "string" || approve !== undefined && typeof approve !== "string")
|
|
5627
|
+
refuseDiscussion("invalid_selection", "Supply --input with a strict JSON selection document and an optional full --approve digest.");
|
|
5628
|
+
let selection;
|
|
5629
|
+
try {
|
|
5630
|
+
selection = readLabelMigrationJson(input);
|
|
5631
|
+
}
|
|
5632
|
+
catch {
|
|
5633
|
+
refuseDiscussion("invalid_selection", "Supply a readable bounded regular JSON selection file; see pm github discussion capture --help.");
|
|
5634
|
+
}
|
|
5635
|
+
return captureGithubDiscussion(ctx.pm_root, selection, ctx.sdk, {
|
|
5636
|
+
approval: approve, maxOutputBytes: maxOutputBytes === undefined ? undefined : Number(maxOutputBytes),
|
|
5637
|
+
});
|
|
5638
|
+
},
|
|
5639
|
+
});
|
|
5578
5640
|
api.registerCommand({
|
|
5579
5641
|
name: "github labels plan",
|
|
5580
5642
|
description: "Preview exact provider label rename/consolidation impacts on stored tags and declared filters/mappings. Read-only; --approve revalidates a saved digest against current state.",
|
|
@@ -5763,6 +5825,19 @@ export default defineExtension({
|
|
|
5763
5825
|
return report;
|
|
5764
5826
|
},
|
|
5765
5827
|
});
|
|
5828
|
+
// Receipts stay private until the workflow scans the selected diagnostic.
|
|
5829
|
+
api.registerCommand({
|
|
5830
|
+
name: "github workflow-diagnostic",
|
|
5831
|
+
description: "Select one actionable refusal from private workflow receipts. Privacy-scan before publishing the result.",
|
|
5832
|
+
flags: [
|
|
5833
|
+
{ long: "--report", value_name: "file", description: "Command JSON stdout receipt" },
|
|
5834
|
+
{ long: "--error-file", value_name: "file", description: "Command stderr receipt" },
|
|
5835
|
+
{ long: "--diff", value_name: "file", description: "Repeat-import tracker diff" },
|
|
5836
|
+
],
|
|
5837
|
+
async run(ctx) {
|
|
5838
|
+
return readWorkflowFailure(optionString(ctx.options, "report"), optionString(ctx.options, "error-file", "errorFile"), optionString(ctx.options, "diff"));
|
|
5839
|
+
},
|
|
5840
|
+
});
|
|
5766
5841
|
// Closeout uses the existing repository resolution and REST transport.
|
|
5767
5842
|
api.registerCommand({
|
|
5768
5843
|
name: "github closeout",
|
|
@@ -5785,8 +5860,12 @@ export default defineExtension({
|
|
|
5785
5860
|
const report = await runCloseout(ctx.pm_root, resolveSearchRepo(ctx.options), ctx.options, {
|
|
5786
5861
|
base: githubApiBase(), get: url => fetchJSON(url, token), next: parseNextLink,
|
|
5787
5862
|
});
|
|
5788
|
-
if (report.failed > 0)
|
|
5863
|
+
if (report.failed > 0) {
|
|
5864
|
+
// Preserve the full private receipt before the host formats or budgets its error message.
|
|
5865
|
+
if (ctx.global.json === true)
|
|
5866
|
+
process.stdout.write(`${JSON.stringify(report)}\n`);
|
|
5789
5867
|
throw new CommandError(JSON.stringify({ ...report, error: "pm github closeout: some closes failed" }), EXIT_CODE.GENERIC_FAILURE);
|
|
5868
|
+
}
|
|
5790
5869
|
return ctx.global.json === true || ctx.options.full === true ? report : compactCloseoutReport(report);
|
|
5791
5870
|
}
|
|
5792
5871
|
catch (error) {
|