pm-github 2026.10.25 → 2026.10.27
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 +95 -1
- package/dist/closeout.d.ts.map +1 -1
- package/dist/closeout.js +10 -7
- package/dist/closeout.js.map +1 -1
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +5 -3
- package/dist/gate.js.map +1 -1
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +3 -2
- package/dist/identity.js.map +1 -1
- package/dist/import-fidelity.d.ts +1 -1
- package/dist/import-fidelity.d.ts.map +1 -1
- package/dist/import-fidelity.js +39 -50
- package/dist/import-fidelity.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +66 -39
- package/dist/index.js.map +1 -1
- package/dist/issue-forms.d.ts +72 -0
- package/dist/issue-forms.d.ts.map +1 -0
- package/dist/issue-forms.js +195 -0
- package/dist/issue-forms.js.map +1 -0
- package/dist/own-record.d.ts +8 -0
- package/dist/own-record.d.ts.map +1 -0
- package/dist/own-record.js +14 -0
- package/dist/own-record.js.map +1 -0
- package/dist/projects.d.ts.map +1 -1
- package/dist/projects.js +2 -15
- package/dist/projects.js.map +1 -1
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +3 -2
- package/dist/refresh.js.map +1 -1
- package/dist/webhooks.js +1 -1
- package/dist/webhooks.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +10 -5
package/dist/index.js
CHANGED
|
@@ -1,19 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// Capabilities (see manifest.json):
|
|
4
|
-
// commands — `pm gh-issues import` (legacy) + `pm github sync` +
|
|
5
|
-
// `pm github gate` (fail-closed pre-push privacy gate) +
|
|
6
|
-
// `pm github project list|fields|import|sync` (Projects v2)
|
|
7
|
-
// importers — `pm github import <owner/repo>` (idempotent native import)
|
|
8
|
-
// exporters — `pm github export` (render pm items as a GitHub-issues payload)
|
|
9
|
-
// schema — declares github_url / github_number / github_state /
|
|
10
|
-
// github_author / github_created_at / github_updated_at item fields
|
|
11
|
-
// hooks — afterCommand: actionable sync hint for github-linked items
|
|
12
|
-
// preflight — local guard for mutating github commands (token presence)
|
|
13
|
-
//
|
|
14
|
-
// Issues use the REST API; Projects v2 is GraphQL-only (see the Projects v2
|
|
15
|
-
// section below and the pure plan/mapping logic in ./projects.ts).
|
|
1
|
+
import { ownValue } from "./own-record.js";
|
|
16
2
|
import { CloseoutUsageError, compactCloseoutReport, runCloseout } from "./closeout.js";
|
|
3
|
+
import { compileIssueForm, completeIssueForm, readRepositoryIssueForm, refuseIssueForm } from "./issue-forms.js";
|
|
4
|
+
export { compileIssueForm, completeIssueForm, readRepositoryIssueForm } from "./issue-forms.js";
|
|
17
5
|
import { AmbiguousCitationError, buildImportFidelity, fetchImportTimeline, importFidelityFields, matchImportedIssue, persistImportFidelity, readImportReleases, selectImportContextIndex, validateImportLifetime, } from "./import-fidelity.js";
|
|
18
6
|
import { refreshChanges, runRefresh } from "./refresh.js";
|
|
19
7
|
import { exploreGithubIdentities, planGithubIdentityRepair, applyGithubIdentityRepair } from "./identity.js";
|
|
@@ -216,7 +204,7 @@ function requestOnce(method, url, token, payload, redirectsLeft = 5) {
|
|
|
216
204
|
*/
|
|
217
205
|
export function computeBackoffMs(headers, attempt, nowMs = Date.now()) {
|
|
218
206
|
const get = (k) => {
|
|
219
|
-
const v = headers
|
|
207
|
+
const v = ownValue(headers, k) ?? ownValue(headers, k.toLowerCase());
|
|
220
208
|
return Array.isArray(v) ? v[0] : v;
|
|
221
209
|
};
|
|
222
210
|
const cap = 60_000;
|
|
@@ -245,11 +233,11 @@ export function parseRateLimit(headers, lowThreshold = 10) {
|
|
|
245
233
|
// Node lowercases response header names, but be defensive: scan
|
|
246
234
|
// case-insensitively so a mixed-case key (e.g. from a different runtime or a
|
|
247
235
|
// mocked response) is still found.
|
|
248
|
-
const lower =
|
|
236
|
+
const lower = new Map();
|
|
249
237
|
for (const [hk, hv] of Object.entries(headers))
|
|
250
|
-
lower
|
|
238
|
+
lower.set(hk.toLowerCase(), hv);
|
|
251
239
|
const get = (k) => {
|
|
252
|
-
const v = lower
|
|
240
|
+
const v = lower.get(k.toLowerCase());
|
|
253
241
|
return Array.isArray(v) ? v[0] : v;
|
|
254
242
|
};
|
|
255
243
|
const num = (s) => {
|
|
@@ -310,11 +298,11 @@ export function isRetryableStatus(status, headers) {
|
|
|
310
298
|
return true;
|
|
311
299
|
// Secondary/primary rate limit surfaces as 403 with remaining=0.
|
|
312
300
|
if (status === 403) {
|
|
313
|
-
const v = headers
|
|
301
|
+
const v = ownValue(headers, "x-ratelimit-remaining") ?? ownValue(headers, "X-RateLimit-Remaining");
|
|
314
302
|
const remaining = Array.isArray(v) ? v[0] : v;
|
|
315
303
|
if (remaining === "0")
|
|
316
304
|
return true;
|
|
317
|
-
if (headers
|
|
305
|
+
if (ownValue(headers, "retry-after") ?? ownValue(headers, "Retry-After"))
|
|
318
306
|
return true;
|
|
319
307
|
}
|
|
320
308
|
return false;
|
|
@@ -373,7 +361,7 @@ export function mapState(state, stateReason) {
|
|
|
373
361
|
// key depending on runtime normalization, so check every candidate.
|
|
374
362
|
export function optionEnabled(options, ...keys) {
|
|
375
363
|
return keys.some((k) => {
|
|
376
|
-
const v = options
|
|
364
|
+
const v = ownValue(options, k);
|
|
377
365
|
return v === true || v === "true" || v === "1";
|
|
378
366
|
});
|
|
379
367
|
}
|
|
@@ -381,7 +369,7 @@ export function optionEnabled(options, ...keys) {
|
|
|
381
369
|
// (kebab- or camel-case) keys; returns undefined when none are set.
|
|
382
370
|
export function optionString(options, ...keys) {
|
|
383
371
|
for (const k of keys) {
|
|
384
|
-
const v = options
|
|
372
|
+
const v = ownValue(options, k);
|
|
385
373
|
if (typeof v === "string" && v.trim().length > 0)
|
|
386
374
|
return v.trim();
|
|
387
375
|
}
|
|
@@ -453,7 +441,7 @@ export function applyLabelMap(labels, labelMap) {
|
|
|
453
441
|
export function optionCsv(options, ...keys) {
|
|
454
442
|
const rawChunks = [];
|
|
455
443
|
for (const k of keys) {
|
|
456
|
-
const v = options
|
|
444
|
+
const v = ownValue(options, k);
|
|
457
445
|
if (typeof v === "string") {
|
|
458
446
|
rawChunks.push(v);
|
|
459
447
|
continue;
|
|
@@ -679,7 +667,7 @@ export function decodeCompletePmItems(parsed) {
|
|
|
679
667
|
|| !readOutput.requested_dimensions.includes("cost")) {
|
|
680
668
|
throw new CommandError("Refusing unverifiable pm list --all output: read_output.requested_dimensions must include include, amount, and cost.");
|
|
681
669
|
}
|
|
682
|
-
if ("output_budget_truncation"
|
|
670
|
+
if (Object.hasOwn(parsed, "output_budget_truncation") || Object.hasOwn(parsed, "output_budget_exceeded")) {
|
|
683
671
|
throw new CommandError("Refusing unverifiable pm list --all output: a budget truncation or omission disclosure was present.");
|
|
684
672
|
}
|
|
685
673
|
if (!Number.isSafeInteger(parsed.count) || parsed.count < 0) {
|
|
@@ -832,7 +820,7 @@ export function searchDocumentToItem(document) {
|
|
|
832
820
|
const wrapped = document.metadata;
|
|
833
821
|
if (wrapped && typeof wrapped === "object")
|
|
834
822
|
return wrapped;
|
|
835
|
-
return "id"
|
|
823
|
+
return Object.hasOwn(document, "id") ? document : undefined;
|
|
836
824
|
}
|
|
837
825
|
/**
|
|
838
826
|
* Resolve the corpus the search provider matches remote hits against.
|
|
@@ -3453,14 +3441,12 @@ export function applyOutcomeError(plan, result, repo) {
|
|
|
3453
3441
|
// stdout in JSON mode (pm renders the return value). Used by both the
|
|
3454
3442
|
// `registerExporter("github", ...)` entry point and the `pm github export`
|
|
3455
3443
|
// command so the surface stays consistent.
|
|
3456
|
-
/**
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
* duplicated; `--label-map` translates pm tags to GitHub labels.
|
|
3463
|
-
*/
|
|
3444
|
+
/** Resolve explicit CLI selection and files through the package's read-only REST client. */
|
|
3445
|
+
async function selectedIssueForm(ctx) {
|
|
3446
|
+
const token = resolveGitHubToken();
|
|
3447
|
+
return readRepositoryIssueForm({ repo: optionString(ctx.options, "repo") ?? "", ref: optionString(ctx.options, "form-ref", "formRef") ?? "", path: optionString(ctx.options, "form") ?? "" }, url => fetchJSON(url, token), githubApiBase(), { answers: optionString(ctx.options, "form-answers", "formAnswers"), bindings: optionString(ctx.options, "form-bindings", "formBindings") });
|
|
3448
|
+
}
|
|
3449
|
+
/** Re-read all selected form answers before constructing or applying the export. */
|
|
3464
3450
|
async function runExport(ctx) {
|
|
3465
3451
|
const options = ctx.options;
|
|
3466
3452
|
const jsonMode = ctx.global.json === true;
|
|
@@ -3473,8 +3459,28 @@ async function runExport(ctx) {
|
|
|
3473
3459
|
if (idsProvided && scopedIds.length === 0) {
|
|
3474
3460
|
throw new CommandError("--ids requires at least one pm item id (comma-separated), e.g. --ids pm-123,pm-456.", EXIT_CODE.USAGE);
|
|
3475
3461
|
}
|
|
3476
|
-
const
|
|
3462
|
+
const items = optionProvided(options, "form")
|
|
3463
|
+
? decodeCompletePmItems(certifyCompleteListResult(await listAllComplete({ includeBody: true }, { pmRoot: ctx.pm_root })))
|
|
3464
|
+
: readPmItems(ctx.pm_root);
|
|
3465
|
+
const selected = requireRequestedItems(items, scopedIds);
|
|
3466
|
+
const completions = [];
|
|
3467
|
+
if (optionProvided(options, "form")) {
|
|
3468
|
+
const source = await selectedIssueForm(ctx);
|
|
3469
|
+
compileIssueForm(source);
|
|
3470
|
+
for (const item of selected)
|
|
3471
|
+
completions.push(await completeIssueForm(ctx.pm_root, { ...source, item: item.id }));
|
|
3472
|
+
const repairs = completions.flatMap(completion => completion.report.checklist.map(repair => ({ ...repair, field: `${completion.report.item}:${repair.field}` })));
|
|
3473
|
+
if (repairs.length)
|
|
3474
|
+
refuseIssueForm("incomplete_answers", repairs, completions.reduce((sum, completion) => sum + completion.report.omitted, 0));
|
|
3475
|
+
}
|
|
3476
|
+
else if (["form-ref", "formRef", "form-answers", "formAnswers", "form-bindings", "formBindings"].some(flag => optionProvided(options, flag))) {
|
|
3477
|
+
refuseIssueForm("invalid_form_input", [{ field: "form", reason: "selection", remedy: "Pass --form with --repo and --form-ref to enforce an issue-form contract." }]);
|
|
3478
|
+
}
|
|
3477
3479
|
const plan = buildExportPlan(selected, repo, labelMap);
|
|
3480
|
+
for (const [index, completion] of completions.entries()) {
|
|
3481
|
+
plan[index].payload.body = completion.body;
|
|
3482
|
+
plan[index].payload.labels = [...new Set([...plan[index].payload.labels, ...completion.report.contract.labels])];
|
|
3483
|
+
}
|
|
3478
3484
|
const creates = plan.filter((e) => e.action === "create").length;
|
|
3479
3485
|
const updates = plan.filter((e) => e.action === "update").length;
|
|
3480
3486
|
if (!apply) {
|
|
@@ -3980,12 +3986,12 @@ export async function fetchBlockerReferenceStates(refs, transport) {
|
|
|
3980
3986
|
states.set(entry.ref.key, { ...entry.ref, state: "error", error: "GitHub rejected blocker state access." });
|
|
3981
3987
|
continue;
|
|
3982
3988
|
}
|
|
3983
|
-
const repoBlock = response.data
|
|
3984
|
-
if (repoBlock === undefined || (repoBlock !== null && (repoBlock
|
|
3989
|
+
const repoBlock = ownValue(response.data, entry.repoAlias);
|
|
3990
|
+
if (repoBlock === undefined || (repoBlock !== null && (ownValue(repoBlock, entry.issueAlias) === undefined || ownValue(repoBlock, entry.pullAlias) === undefined))) {
|
|
3985
3991
|
states.set(entry.ref.key, { ...entry.ref, state: "error", error: "Incomplete GitHub blocker state response." });
|
|
3986
3992
|
continue;
|
|
3987
3993
|
}
|
|
3988
|
-
states.set(entry.ref.key, mapBlockerReferenceNode(entry.ref, repoBlock
|
|
3994
|
+
states.set(entry.ref.key, mapBlockerReferenceNode(entry.ref, ownValue(repoBlock, entry.issueAlias), ownValue(repoBlock, entry.pullAlias)));
|
|
3989
3995
|
}
|
|
3990
3996
|
}
|
|
3991
3997
|
return states;
|
|
@@ -5246,6 +5252,10 @@ const IMPORT_FLAGS = [
|
|
|
5246
5252
|
{ long: "--type", value_name: "type", description: "Override pm item type (default: Issue)" },
|
|
5247
5253
|
];
|
|
5248
5254
|
const EXPORT_FLAGS = [
|
|
5255
|
+
{ long: "--form", value_name: "path", description: "Enforce a selected repository issue form before any export writes" },
|
|
5256
|
+
{ long: "--form-ref", value_name: "revision", description: "Explicit repository revision containing the selected form" },
|
|
5257
|
+
{ long: "--form-answers", value_name: "json", description: "JSON map of form answer IDs to strings or string arrays" },
|
|
5258
|
+
{ long: "--form-bindings", value_name: "json", description: "JSON map of form answer IDs to native pm field names" },
|
|
5249
5259
|
{ long: "--repo", value_name: "owner/repo", description: "Target GitHub repo (required for --apply; enables upsert of linked issues)" },
|
|
5250
5260
|
{ long: "--ids", value_name: "pm-1,pm-2", description: "Only export these pm item IDs (comma-separated)" },
|
|
5251
5261
|
{ long: "--apply", description: "Write to GitHub (default is a safe dry-run preview)" },
|
|
@@ -5307,7 +5317,7 @@ const PROJECT_SYNC_FLAGS = [
|
|
|
5307
5317
|
const defineExtension = (module) => module;
|
|
5308
5318
|
export default defineExtension({
|
|
5309
5319
|
name: "pm-github",
|
|
5310
|
-
version: "2026.10.
|
|
5320
|
+
version: "2026.10.27",
|
|
5311
5321
|
activate(api) {
|
|
5312
5322
|
// -----------------------------------------------------------------------
|
|
5313
5323
|
// schema — declare the GitHub provenance fields so the workspace knows them
|
|
@@ -5416,6 +5426,23 @@ export default defineExtension({
|
|
|
5416
5426
|
"Use --ids <pm-1,pm-2> to scope export; unknown IDs fail fast.",
|
|
5417
5427
|
],
|
|
5418
5428
|
});
|
|
5429
|
+
api.registerCommand({
|
|
5430
|
+
name: "github form",
|
|
5431
|
+
description: "Compile and check an explicitly selected repository issue form against current SDK item state. Read-only; returns a compact contract and bounded repair checklist.",
|
|
5432
|
+
flags: [...EXPORT_FLAGS.filter(flag => ["--repo", "--form", "--form-ref", "--form-answers", "--form-bindings"].includes(flag.long)),
|
|
5433
|
+
{ long: "--compile", description: "Only compile requirements, validators and bindings; do not check completion" },
|
|
5434
|
+
{ long: "--item", value_name: "id", description: "Read current native pm fields through the SDK for explicit bindings" }],
|
|
5435
|
+
examples: ["pm github form --repo owner/repo --form .github/ISSUE_TEMPLATE/bug.yml --form-ref main --item pm-1 --form-bindings bindings.json --json"],
|
|
5436
|
+
async run(ctx) {
|
|
5437
|
+
const source = await selectedIssueForm(ctx);
|
|
5438
|
+
if (ctx.options.compile === true)
|
|
5439
|
+
return compileIssueForm(source);
|
|
5440
|
+
const completion = await completeIssueForm(ctx.pm_root, { ...source, item: optionString(ctx.options, "item") });
|
|
5441
|
+
if (!completion.report.ok)
|
|
5442
|
+
refuseIssueForm("incomplete_answers", completion.report.checklist, completion.report.omitted);
|
|
5443
|
+
return completion.report;
|
|
5444
|
+
},
|
|
5445
|
+
});
|
|
5419
5446
|
// -----------------------------------------------------------------------
|
|
5420
5447
|
// search — reach GitHub from `pm search` for imported items.
|
|
5421
5448
|
// Guarded by a capability check so it is a no-op on runtimes that predate
|