hyper-pm 0.1.2 → 0.1.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/index.cjs CHANGED
@@ -7622,6 +7622,20 @@ var envSchema = external_exports.object({
7622
7622
  HYPER_PM_AI_API_KEY: external_exports.string().optional(),
7623
7623
  /** Optional override for hyper-pm JSONL event `actor` on CLI mutations. */
7624
7624
  HYPER_PM_ACTOR: external_exports.string().optional(),
7625
+ /**
7626
+ * Optional `git user.name` for hyper-pm data-branch commits when repo git
7627
+ * identity is unset (falls back after `GIT_AUTHOR_*`, then a built-in default).
7628
+ */
7629
+ HYPER_PM_GIT_USER_NAME: external_exports.string().optional(),
7630
+ /**
7631
+ * Optional `git user.email` for hyper-pm data-branch commits when repo git
7632
+ * identity is unset (falls back after `GIT_AUTHOR_*`, then a built-in default).
7633
+ */
7634
+ HYPER_PM_GIT_USER_EMAIL: external_exports.string().optional(),
7635
+ /** Standard Git override for commit author name (optional). */
7636
+ GIT_AUTHOR_NAME: external_exports.string().optional(),
7637
+ /** Standard Git override for commit author email (optional). */
7638
+ GIT_AUTHOR_EMAIL: external_exports.string().optional(),
7625
7639
  /**
7626
7640
  * Absolute path to the hyper-pm CLI bundle (`main.cjs`) for hyper-pm-mcp and
7627
7641
  * other integrations when auto-resolution from the `hyper-pm` package is insufficient.
@@ -12681,6 +12695,336 @@ var listActiveTicketSummaries = (projection, options) => {
12681
12695
  });
12682
12696
  };
12683
12697
 
12698
+ // src/lib/github-issue-body.ts
12699
+ var FENCE_JSON_RE = /```json\s*([\s\S]*?)```/i;
12700
+ var parseHyperPmFenceObject = (body) => {
12701
+ const fence = body.match(FENCE_JSON_RE);
12702
+ if (!fence?.[1]) return void 0;
12703
+ try {
12704
+ const data = JSON.parse(fence[1].trim());
12705
+ if (typeof data !== "object" || data === null) return void 0;
12706
+ return data;
12707
+ } catch {
12708
+ return void 0;
12709
+ }
12710
+ };
12711
+ var parseHyperPmIdFromIssueBody = (body) => {
12712
+ const meta = parseHyperPmFenceObject(body);
12713
+ if (meta === void 0) return void 0;
12714
+ const id = meta["hyper_pm_id"];
12715
+ return typeof id === "string" ? id : void 0;
12716
+ };
12717
+ var extractDescriptionBeforeFirstFence = (body) => {
12718
+ const fenceIdx = body.indexOf("```");
12719
+ if (fenceIdx === -1) {
12720
+ return body.trim();
12721
+ }
12722
+ return body.slice(0, fenceIdx).trim();
12723
+ };
12724
+ var inboundTicketPlanningPayloadFromFenceMeta = (meta) => {
12725
+ const out = {};
12726
+ if (Object.prototype.hasOwnProperty.call(meta, "priority")) {
12727
+ const v = meta["priority"];
12728
+ if (v === null) {
12729
+ out["priority"] = null;
12730
+ } else if (typeof v === "string") {
12731
+ const p = tryParseTicketPriority(v);
12732
+ if (p !== void 0) {
12733
+ out["priority"] = p;
12734
+ }
12735
+ }
12736
+ }
12737
+ if (Object.prototype.hasOwnProperty.call(meta, "size")) {
12738
+ const v = meta["size"];
12739
+ if (v === null) {
12740
+ out["size"] = null;
12741
+ } else if (typeof v === "string") {
12742
+ const s = tryParseTicketSize(v);
12743
+ if (s !== void 0) {
12744
+ out["size"] = s;
12745
+ }
12746
+ }
12747
+ }
12748
+ if (Object.prototype.hasOwnProperty.call(meta, "estimate")) {
12749
+ const v = meta["estimate"];
12750
+ if (v === null) {
12751
+ out["estimate"] = null;
12752
+ } else if (typeof v === "number" && Number.isFinite(v) && v >= 0) {
12753
+ out["estimate"] = v;
12754
+ }
12755
+ }
12756
+ if (Object.prototype.hasOwnProperty.call(meta, "start_work_at")) {
12757
+ const v = meta["start_work_at"];
12758
+ if (v === null) {
12759
+ out["startWorkAt"] = null;
12760
+ } else if (typeof v === "string") {
12761
+ const t = v.trim();
12762
+ if (t !== "" && Number.isFinite(Date.parse(t))) {
12763
+ out["startWorkAt"] = t;
12764
+ }
12765
+ }
12766
+ }
12767
+ if (Object.prototype.hasOwnProperty.call(meta, "target_finish_at")) {
12768
+ const v = meta["target_finish_at"];
12769
+ if (v === null) {
12770
+ out["targetFinishAt"] = null;
12771
+ } else if (typeof v === "string") {
12772
+ const t = v.trim();
12773
+ if (t !== "" && Number.isFinite(Date.parse(t))) {
12774
+ out["targetFinishAt"] = t;
12775
+ }
12776
+ }
12777
+ }
12778
+ return out;
12779
+ };
12780
+ var buildGithubIssueBody = (params) => {
12781
+ const meta = {
12782
+ hyper_pm_id: params.hyperPmId,
12783
+ type: params.type,
12784
+ parent_ids: params.parentIds
12785
+ };
12786
+ if (params.type === "ticket" && params.ticketPlanning !== void 0) {
12787
+ const p = params.ticketPlanning;
12788
+ if (p.priority !== void 0) {
12789
+ meta.priority = p.priority;
12790
+ }
12791
+ if (p.size !== void 0) {
12792
+ meta.size = p.size;
12793
+ }
12794
+ if (p.estimate !== void 0) {
12795
+ meta.estimate = p.estimate;
12796
+ }
12797
+ if (p.startWorkAt !== void 0) {
12798
+ meta.start_work_at = p.startWorkAt;
12799
+ }
12800
+ if (p.targetFinishAt !== void 0) {
12801
+ meta.target_finish_at = p.targetFinishAt;
12802
+ }
12803
+ }
12804
+ return `${params.description.trim()}
12805
+
12806
+ \`\`\`json
12807
+ ${JSON.stringify(meta, null, 2)}
12808
+ \`\`\`
12809
+ `;
12810
+ };
12811
+ var ticketPlanningForGithubIssueBody = (ticket) => {
12812
+ const out = {};
12813
+ if (ticket.priority !== void 0) {
12814
+ out.priority = ticket.priority;
12815
+ }
12816
+ if (ticket.size !== void 0) {
12817
+ out.size = ticket.size;
12818
+ }
12819
+ if (ticket.estimate !== void 0) {
12820
+ out.estimate = ticket.estimate;
12821
+ }
12822
+ if (ticket.startWorkAt !== void 0) {
12823
+ out.startWorkAt = ticket.startWorkAt;
12824
+ }
12825
+ if (ticket.targetFinishAt !== void 0) {
12826
+ out.targetFinishAt = ticket.targetFinishAt;
12827
+ }
12828
+ return Object.keys(out).length > 0 ? out : void 0;
12829
+ };
12830
+
12831
+ // src/lib/github-issue-labels.ts
12832
+ var RESERVED_LOWER = /* @__PURE__ */ new Set(["hyper-pm", "ticket"]);
12833
+ var GITHUB_LABEL_NAME_MAX_LENGTH = 50;
12834
+ var isReservedHyperPmGithubLabel = (name) => {
12835
+ return RESERVED_LOWER.has(name.trim().toLowerCase());
12836
+ };
12837
+ var labelNameFromGithubLabelEntry = (entry) => {
12838
+ if (typeof entry === "string") {
12839
+ const t = entry.trim();
12840
+ return t === "" ? void 0 : t;
12841
+ }
12842
+ if (typeof entry === "object" && entry !== null && "name" in entry) {
12843
+ const n = entry.name;
12844
+ if (typeof n !== "string") return void 0;
12845
+ const t = n.trim();
12846
+ return t === "" ? void 0 : t;
12847
+ }
12848
+ return void 0;
12849
+ };
12850
+ var ticketLabelsFromGithubIssueLabels = (labels) => {
12851
+ if (!Array.isArray(labels)) {
12852
+ return [];
12853
+ }
12854
+ const raw = [];
12855
+ for (const item of labels) {
12856
+ const n = labelNameFromGithubLabelEntry(item);
12857
+ if (n === void 0) continue;
12858
+ if (isReservedHyperPmGithubLabel(n)) continue;
12859
+ raw.push(n);
12860
+ }
12861
+ return normalizeTicketLabelList(raw);
12862
+ };
12863
+ var mergeOutboundGithubIssueLabelsForTicket = (ticketLabels) => {
12864
+ const base = ["hyper-pm", "ticket"];
12865
+ const seen = new Set(base.map((x) => x.toLowerCase()));
12866
+ const out = [...base];
12867
+ const norm = normalizeTicketLabelList(ticketLabels ?? []);
12868
+ for (const lab of norm) {
12869
+ if (lab.length > GITHUB_LABEL_NAME_MAX_LENGTH) {
12870
+ continue;
12871
+ }
12872
+ const low = lab.toLowerCase();
12873
+ if (seen.has(low)) continue;
12874
+ seen.add(low);
12875
+ out.push(lab);
12876
+ }
12877
+ return out;
12878
+ };
12879
+
12880
+ // src/cli/github-issue-import.ts
12881
+ var collectLinkedGithubIssueNumbers = (projection) => {
12882
+ const out = /* @__PURE__ */ new Set();
12883
+ for (const ticket of projection.tickets.values()) {
12884
+ if (ticket.deleted) continue;
12885
+ const n = ticket.githubIssueNumber;
12886
+ if (n !== void 0 && Number.isFinite(n)) {
12887
+ out.add(n);
12888
+ }
12889
+ }
12890
+ return out;
12891
+ };
12892
+ var stripHyperPmGithubIssueTitle = (title) => title.replace(/^\[hyper-pm\]\s*/i, "").trim();
12893
+ var tryParseGithubImportListState = (raw) => {
12894
+ if (raw === void 0 || raw === "") return "all";
12895
+ const t = raw.trim().toLowerCase();
12896
+ if (t === "open" || t === "closed" || t === "all") return t;
12897
+ return void 0;
12898
+ };
12899
+ var parseGithubImportIssueNumberSet = (raw) => {
12900
+ if (raw === void 0 || raw.length === 0) return void 0;
12901
+ const out = /* @__PURE__ */ new Set();
12902
+ for (const piece of raw) {
12903
+ for (const token of piece.split(",")) {
12904
+ const t = token.trim();
12905
+ if (t === "") continue;
12906
+ const n = Number.parseInt(t, 10);
12907
+ if (!Number.isFinite(n) || n < 1) {
12908
+ throw new Error(`Invalid --issue value: ${JSON.stringify(token)}`);
12909
+ }
12910
+ out.add(n);
12911
+ }
12912
+ }
12913
+ if (out.size === 0) {
12914
+ throw new Error("No valid --issue numbers after parsing flags");
12915
+ }
12916
+ return out;
12917
+ };
12918
+ var ticketCreatePlanningFragmentFromFenceMeta = (meta) => {
12919
+ if (meta === void 0) return {};
12920
+ const src = inboundTicketPlanningPayloadFromFenceMeta(meta);
12921
+ const out = {};
12922
+ for (const [k, v] of Object.entries(src)) {
12923
+ if (v !== null && v !== void 0) {
12924
+ out[k] = v;
12925
+ }
12926
+ }
12927
+ return out;
12928
+ };
12929
+ var buildTicketCreatedPayloadBaseFromGithubIssue = (issue) => {
12930
+ const bodyText = issue.body ?? "";
12931
+ const title = stripHyperPmGithubIssueTitle(String(issue.title ?? ""));
12932
+ const desc = extractDescriptionBeforeFirstFence(bodyText);
12933
+ const payload = {
12934
+ title,
12935
+ body: desc
12936
+ };
12937
+ if (issue.state === "closed") {
12938
+ payload["state"] = "closed";
12939
+ }
12940
+ const assignee = assigneeFromGithubIssue(issue);
12941
+ if (assignee !== void 0 && assignee !== "") {
12942
+ payload["assignee"] = assignee;
12943
+ }
12944
+ const labels = ticketLabelsFromGithubIssueLabels(issue.labels);
12945
+ if (labels.length > 0) {
12946
+ payload["labels"] = labels;
12947
+ }
12948
+ const meta = parseHyperPmFenceObject(bodyText);
12949
+ Object.assign(payload, ticketCreatePlanningFragmentFromFenceMeta(meta));
12950
+ return payload;
12951
+ };
12952
+ var classifyGithubIssueForImport = (params) => {
12953
+ const num = params.issue.number;
12954
+ if (!Number.isFinite(num) || num < 1) {
12955
+ return {
12956
+ result: "skip",
12957
+ skip: { issueNumber: 0, reason: "issue_filter" }
12958
+ };
12959
+ }
12960
+ if (params.onlyIssueNumbers !== void 0 && !params.onlyIssueNumbers.has(num)) {
12961
+ return {
12962
+ result: "skip",
12963
+ skip: { issueNumber: num, reason: "issue_filter" }
12964
+ };
12965
+ }
12966
+ if (params.issue.pull_request !== void 0 && params.issue.pull_request !== null) {
12967
+ return {
12968
+ result: "skip",
12969
+ skip: { issueNumber: num, reason: "pull_request" }
12970
+ };
12971
+ }
12972
+ if (params.linkedNumbers.has(num)) {
12973
+ return {
12974
+ result: "skip",
12975
+ skip: { issueNumber: num, reason: "already_linked" }
12976
+ };
12977
+ }
12978
+ const body = params.issue.body ?? "";
12979
+ const hyperPmId = parseHyperPmIdFromIssueBody(body);
12980
+ if (hyperPmId !== void 0 && hyperPmId.trim() !== "") {
12981
+ const row = params.projection.tickets.get(hyperPmId);
12982
+ if (row !== void 0 && !row.deleted) {
12983
+ return {
12984
+ result: "skip",
12985
+ skip: { issueNumber: num, reason: "body_hyper_pm_existing_ticket" }
12986
+ };
12987
+ }
12988
+ return {
12989
+ result: "skip",
12990
+ skip: { issueNumber: num, reason: "body_hyper_pm_orphan_ref" }
12991
+ };
12992
+ }
12993
+ return {
12994
+ result: "candidate",
12995
+ ticketCreatedPayloadBase: buildTicketCreatedPayloadBaseFromGithubIssue(
12996
+ params.issue
12997
+ )
12998
+ };
12999
+ };
13000
+ var partitionGithubIssuesForImport = (params) => {
13001
+ const linkedNumbers = collectLinkedGithubIssueNumbers(params.projection);
13002
+ const candidates = [];
13003
+ const skipped = [];
13004
+ for (const issue of params.issues) {
13005
+ const r = classifyGithubIssueForImport({
13006
+ projection: params.projection,
13007
+ linkedNumbers,
13008
+ onlyIssueNumbers: params.onlyIssueNumbers,
13009
+ issue
13010
+ });
13011
+ if (r.result === "skip") {
13012
+ skipped.push(r.skip);
13013
+ } else {
13014
+ candidates.push({
13015
+ issueNumber: issue.number,
13016
+ ticketCreatedPayloadBase: r.ticketCreatedPayloadBase
13017
+ });
13018
+ }
13019
+ }
13020
+ return { candidates, skipped };
13021
+ };
13022
+ var mergeTicketImportCreatePayload = (ticketId, base, storyId) => {
13023
+ const storyTrimmed = storyId !== void 0 && storyId !== "" ? storyId.trim() : void 0;
13024
+ const storyPayload = storyTrimmed !== void 0 && storyTrimmed !== "" ? { storyId: storyTrimmed } : {};
13025
+ return { id: ticketId, ...base, ...storyPayload };
13026
+ };
13027
+
12684
13028
  // src/config/hyper-pm-config.ts
12685
13029
  var hyperPmConfigSchema = external_exports.object({
12686
13030
  schema: external_exports.literal(1),
@@ -13033,6 +13377,33 @@ var tryReadGithubOwnerRepoSlugFromGit = async (params) => {
13033
13377
  }
13034
13378
  };
13035
13379
 
13380
+ // src/git/resolve-effective-git-author-for-data-commit.ts
13381
+ var defaultDataCommitName = "hyper-pm";
13382
+ var defaultDataCommitEmail = "hyper-pm@users.noreply.github.com";
13383
+ var tryReadGitConfigUserName = async (cwd, runGit2) => {
13384
+ try {
13385
+ const { stdout } = await runGit2(cwd, ["config", "--get", "user.name"]);
13386
+ return stdout.trim();
13387
+ } catch {
13388
+ return "";
13389
+ }
13390
+ };
13391
+ var tryReadGitConfigUserEmail = async (cwd, runGit2) => {
13392
+ try {
13393
+ const { stdout } = await runGit2(cwd, ["config", "--get", "user.email"]);
13394
+ return stdout.trim();
13395
+ } catch {
13396
+ return "";
13397
+ }
13398
+ };
13399
+ var resolveEffectiveGitAuthorForDataCommit = async (cwd, runGit2, authorEnv) => {
13400
+ const fromGitName = await tryReadGitConfigUserName(cwd, runGit2);
13401
+ const fromGitEmail = await tryReadGitConfigUserEmail(cwd, runGit2);
13402
+ const name = fromGitName || authorEnv.HYPER_PM_GIT_USER_NAME?.trim() || authorEnv.GIT_AUTHOR_NAME?.trim() || defaultDataCommitName;
13403
+ const email = fromGitEmail || authorEnv.HYPER_PM_GIT_USER_EMAIL?.trim() || authorEnv.GIT_AUTHOR_EMAIL?.trim() || defaultDataCommitEmail;
13404
+ return { name, email };
13405
+ };
13406
+
13036
13407
  // src/run/commit-data.ts
13037
13408
  var maxActorSuffixLen = 60;
13038
13409
  var formatDataBranchCommitMessage = (base, actorSuffix) => {
@@ -13044,15 +13415,55 @@ var formatDataBranchCommitMessage = (base, actorSuffix) => {
13044
13415
  const suffix = collapsed.length > maxActorSuffixLen ? `${collapsed.slice(0, maxActorSuffixLen - 1)}\u2026` : collapsed;
13045
13416
  return `${base} (${suffix})`;
13046
13417
  };
13047
- var commitDataWorktreeIfNeeded = async (worktreePath, message, runGit2) => {
13418
+ var commitDataWorktreeIfNeeded = async (worktreePath, message, runGit2, opts) => {
13048
13419
  const { stdout } = await runGit2(worktreePath, ["status", "--porcelain"]);
13049
13420
  if (!stdout.trim()) return;
13050
13421
  await runGit2(worktreePath, ["add", "."]);
13051
- await runGit2(worktreePath, ["commit", "-m", message]);
13052
- };
13053
-
13054
- // src/storage/append-event.ts
13055
- var import_promises5 = require("node:fs/promises");
13422
+ const authorEnv = opts?.authorEnv ?? env;
13423
+ const { name, email } = await resolveEffectiveGitAuthorForDataCommit(
13424
+ worktreePath,
13425
+ runGit2,
13426
+ authorEnv
13427
+ );
13428
+ await runGit2(worktreePath, [
13429
+ "-c",
13430
+ `user.name=${name}`,
13431
+ "-c",
13432
+ `user.email=${email}`,
13433
+ "commit",
13434
+ "-m",
13435
+ message
13436
+ ]);
13437
+ };
13438
+
13439
+ // src/run/push-data-branch.ts
13440
+ var firstLineFromUnknown = (err) => {
13441
+ const raw = err instanceof Error ? err.message : String(err);
13442
+ const line = raw.trim().split("\n")[0]?.trim() ?? raw.trim();
13443
+ return line.length > 0 ? line : "git error";
13444
+ };
13445
+ var tryPushDataBranchToRemote = async (worktreePath, remote, branch, runGit2) => {
13446
+ try {
13447
+ await runGit2(worktreePath, ["remote", "get-url", remote]);
13448
+ } catch (e) {
13449
+ return {
13450
+ status: "skipped_no_remote",
13451
+ detail: firstLineFromUnknown(e)
13452
+ };
13453
+ }
13454
+ try {
13455
+ await runGit2(worktreePath, ["push", "-u", remote, branch]);
13456
+ return { status: "pushed" };
13457
+ } catch (e) {
13458
+ return {
13459
+ status: "failed",
13460
+ detail: firstLineFromUnknown(e)
13461
+ };
13462
+ }
13463
+ };
13464
+
13465
+ // src/storage/append-event.ts
13466
+ var import_promises5 = require("node:fs/promises");
13056
13467
  var import_node_path5 = require("node:path");
13057
13468
 
13058
13469
  // src/storage/event-path.ts
@@ -13765,188 +14176,6 @@ var defaultGithubPrActivitySyncDeps = (params) => ({
13765
14176
  }
13766
14177
  });
13767
14178
 
13768
- // src/lib/github-issue-body.ts
13769
- var FENCE_JSON_RE = /```json\s*([\s\S]*?)```/i;
13770
- var parseHyperPmFenceObject = (body) => {
13771
- const fence = body.match(FENCE_JSON_RE);
13772
- if (!fence?.[1]) return void 0;
13773
- try {
13774
- const data = JSON.parse(fence[1].trim());
13775
- if (typeof data !== "object" || data === null) return void 0;
13776
- return data;
13777
- } catch {
13778
- return void 0;
13779
- }
13780
- };
13781
- var parseHyperPmIdFromIssueBody = (body) => {
13782
- const meta = parseHyperPmFenceObject(body);
13783
- if (meta === void 0) return void 0;
13784
- const id = meta["hyper_pm_id"];
13785
- return typeof id === "string" ? id : void 0;
13786
- };
13787
- var extractDescriptionBeforeFirstFence = (body) => {
13788
- const fenceIdx = body.indexOf("```");
13789
- if (fenceIdx === -1) {
13790
- return body.trim();
13791
- }
13792
- return body.slice(0, fenceIdx).trim();
13793
- };
13794
- var inboundTicketPlanningPayloadFromFenceMeta = (meta) => {
13795
- const out = {};
13796
- if (Object.prototype.hasOwnProperty.call(meta, "priority")) {
13797
- const v = meta["priority"];
13798
- if (v === null) {
13799
- out["priority"] = null;
13800
- } else if (typeof v === "string") {
13801
- const p = tryParseTicketPriority(v);
13802
- if (p !== void 0) {
13803
- out["priority"] = p;
13804
- }
13805
- }
13806
- }
13807
- if (Object.prototype.hasOwnProperty.call(meta, "size")) {
13808
- const v = meta["size"];
13809
- if (v === null) {
13810
- out["size"] = null;
13811
- } else if (typeof v === "string") {
13812
- const s = tryParseTicketSize(v);
13813
- if (s !== void 0) {
13814
- out["size"] = s;
13815
- }
13816
- }
13817
- }
13818
- if (Object.prototype.hasOwnProperty.call(meta, "estimate")) {
13819
- const v = meta["estimate"];
13820
- if (v === null) {
13821
- out["estimate"] = null;
13822
- } else if (typeof v === "number" && Number.isFinite(v) && v >= 0) {
13823
- out["estimate"] = v;
13824
- }
13825
- }
13826
- if (Object.prototype.hasOwnProperty.call(meta, "start_work_at")) {
13827
- const v = meta["start_work_at"];
13828
- if (v === null) {
13829
- out["startWorkAt"] = null;
13830
- } else if (typeof v === "string") {
13831
- const t = v.trim();
13832
- if (t !== "" && Number.isFinite(Date.parse(t))) {
13833
- out["startWorkAt"] = t;
13834
- }
13835
- }
13836
- }
13837
- if (Object.prototype.hasOwnProperty.call(meta, "target_finish_at")) {
13838
- const v = meta["target_finish_at"];
13839
- if (v === null) {
13840
- out["targetFinishAt"] = null;
13841
- } else if (typeof v === "string") {
13842
- const t = v.trim();
13843
- if (t !== "" && Number.isFinite(Date.parse(t))) {
13844
- out["targetFinishAt"] = t;
13845
- }
13846
- }
13847
- }
13848
- return out;
13849
- };
13850
- var buildGithubIssueBody = (params) => {
13851
- const meta = {
13852
- hyper_pm_id: params.hyperPmId,
13853
- type: params.type,
13854
- parent_ids: params.parentIds
13855
- };
13856
- if (params.type === "ticket" && params.ticketPlanning !== void 0) {
13857
- const p = params.ticketPlanning;
13858
- if (p.priority !== void 0) {
13859
- meta.priority = p.priority;
13860
- }
13861
- if (p.size !== void 0) {
13862
- meta.size = p.size;
13863
- }
13864
- if (p.estimate !== void 0) {
13865
- meta.estimate = p.estimate;
13866
- }
13867
- if (p.startWorkAt !== void 0) {
13868
- meta.start_work_at = p.startWorkAt;
13869
- }
13870
- if (p.targetFinishAt !== void 0) {
13871
- meta.target_finish_at = p.targetFinishAt;
13872
- }
13873
- }
13874
- return `${params.description.trim()}
13875
-
13876
- \`\`\`json
13877
- ${JSON.stringify(meta, null, 2)}
13878
- \`\`\`
13879
- `;
13880
- };
13881
- var ticketPlanningForGithubIssueBody = (ticket) => {
13882
- const out = {};
13883
- if (ticket.priority !== void 0) {
13884
- out.priority = ticket.priority;
13885
- }
13886
- if (ticket.size !== void 0) {
13887
- out.size = ticket.size;
13888
- }
13889
- if (ticket.estimate !== void 0) {
13890
- out.estimate = ticket.estimate;
13891
- }
13892
- if (ticket.startWorkAt !== void 0) {
13893
- out.startWorkAt = ticket.startWorkAt;
13894
- }
13895
- if (ticket.targetFinishAt !== void 0) {
13896
- out.targetFinishAt = ticket.targetFinishAt;
13897
- }
13898
- return Object.keys(out).length > 0 ? out : void 0;
13899
- };
13900
-
13901
- // src/lib/github-issue-labels.ts
13902
- var RESERVED_LOWER = /* @__PURE__ */ new Set(["hyper-pm", "ticket"]);
13903
- var GITHUB_LABEL_NAME_MAX_LENGTH = 50;
13904
- var isReservedHyperPmGithubLabel = (name) => {
13905
- return RESERVED_LOWER.has(name.trim().toLowerCase());
13906
- };
13907
- var labelNameFromGithubLabelEntry = (entry) => {
13908
- if (typeof entry === "string") {
13909
- const t = entry.trim();
13910
- return t === "" ? void 0 : t;
13911
- }
13912
- if (typeof entry === "object" && entry !== null && "name" in entry) {
13913
- const n = entry.name;
13914
- if (typeof n !== "string") return void 0;
13915
- const t = n.trim();
13916
- return t === "" ? void 0 : t;
13917
- }
13918
- return void 0;
13919
- };
13920
- var ticketLabelsFromGithubIssueLabels = (labels) => {
13921
- if (!Array.isArray(labels)) {
13922
- return [];
13923
- }
13924
- const raw = [];
13925
- for (const item of labels) {
13926
- const n = labelNameFromGithubLabelEntry(item);
13927
- if (n === void 0) continue;
13928
- if (isReservedHyperPmGithubLabel(n)) continue;
13929
- raw.push(n);
13930
- }
13931
- return normalizeTicketLabelList(raw);
13932
- };
13933
- var mergeOutboundGithubIssueLabelsForTicket = (ticketLabels) => {
13934
- const base = ["hyper-pm", "ticket"];
13935
- const seen = new Set(base.map((x) => x.toLowerCase()));
13936
- const out = [...base];
13937
- const norm = normalizeTicketLabelList(ticketLabels ?? []);
13938
- for (const lab of norm) {
13939
- if (lab.length > GITHUB_LABEL_NAME_MAX_LENGTH) {
13940
- continue;
13941
- }
13942
- const low = lab.toLowerCase();
13943
- if (seen.has(low)) continue;
13944
- seen.add(low);
13945
- out.push(lab);
13946
- }
13947
- return out;
13948
- };
13949
-
13950
14179
  // src/sync/github-inbound-actor.ts
13951
14180
  var githubInboundActorFromIssue = (issue) => {
13952
14181
  const login = issue.user?.login?.trim();
@@ -15228,7 +15457,174 @@ ${body}`
15228
15457
  return { id: o.id, deleted: true };
15229
15458
  });
15230
15459
  });
15231
- program2.command("sync").description("GitHub Issues sync").option("--no-github", "skip network sync", false).action(async function() {
15460
+ ticket.command("import-github").description(
15461
+ "Create local tickets for GitHub issues not yet represented in hyper-pm"
15462
+ ).option("--dry-run", "list import candidates without writing events", false).option(
15463
+ "--story <id>",
15464
+ "optional story id applied to every imported ticket (must exist)"
15465
+ ).option(
15466
+ "--state <s>",
15467
+ "GitHub list filter: open | closed | all (default all)",
15468
+ "all"
15469
+ ).option(
15470
+ "--issue <n>",
15471
+ "only consider these issue numbers (repeatable or comma-separated)",
15472
+ (value, previous) => [...previous, value],
15473
+ []
15474
+ ).action(async function() {
15475
+ const g = readGlobals(this);
15476
+ const o = this.opts();
15477
+ const listState = tryParseGithubImportListState(o.state);
15478
+ if (listState === void 0) {
15479
+ deps.error(
15480
+ `Invalid --state ${JSON.stringify(o.state)} (use open, closed, or all)`
15481
+ );
15482
+ deps.exit(ExitCode.UserError);
15483
+ }
15484
+ let onlyIssueNumbers;
15485
+ try {
15486
+ onlyIssueNumbers = parseGithubImportIssueNumberSet(o.issue);
15487
+ } catch (e) {
15488
+ deps.error(e instanceof Error ? e.message : String(e));
15489
+ deps.exit(ExitCode.UserError);
15490
+ }
15491
+ const repoRoot = await resolveRepoRoot(g.repo);
15492
+ const cfg = await loadMergedConfig(repoRoot, g);
15493
+ if (cfg.issueMapping !== "ticket") {
15494
+ deps.error(
15495
+ 'ticket import-github requires config issueMapping "ticket" (other mappings are not supported yet).'
15496
+ );
15497
+ deps.exit(ExitCode.UserError);
15498
+ }
15499
+ const githubToken = await resolveGithubTokenForSync({
15500
+ envToken: env.GITHUB_TOKEN,
15501
+ cwd: repoRoot
15502
+ });
15503
+ if (!githubToken) {
15504
+ deps.error(
15505
+ "GitHub auth required: set GITHUB_TOKEN or run `gh auth login`"
15506
+ );
15507
+ deps.exit(ExitCode.EnvironmentAuth);
15508
+ }
15509
+ const actor = await resolveCliActor({
15510
+ repoRoot,
15511
+ cliActor: g.actor,
15512
+ envActor: env.HYPER_PM_ACTOR
15513
+ });
15514
+ const tmpBase = g.tempDir ?? env.TMPDIR ?? (0, import_node_os2.tmpdir)();
15515
+ const session = await openDataBranchWorktree({
15516
+ repoRoot,
15517
+ dataBranch: cfg.dataBranch,
15518
+ tmpBase,
15519
+ keepWorktree: g.keepWorktree,
15520
+ runGit
15521
+ });
15522
+ try {
15523
+ const lines = await readAllEventLines(session.worktreePath);
15524
+ const proj = replayEvents(lines);
15525
+ const storyRaw = o.story;
15526
+ const storyTrimmed = storyRaw !== void 0 && storyRaw !== "" ? storyRaw.trim() : void 0;
15527
+ if (storyTrimmed !== void 0 && storyTrimmed !== "") {
15528
+ const storyRow = proj.stories.get(storyTrimmed);
15529
+ if (!storyRow || storyRow.deleted) {
15530
+ deps.error(`Story not found: ${storyTrimmed}`);
15531
+ deps.exit(ExitCode.UserError);
15532
+ }
15533
+ }
15534
+ try {
15535
+ const gitDerivedSlug = await tryReadGithubOwnerRepoSlugFromGit({
15536
+ repoRoot,
15537
+ remote: cfg.remote,
15538
+ runGit
15539
+ });
15540
+ const { owner, repo: repo2 } = resolveGithubRepo(
15541
+ cfg,
15542
+ env.GITHUB_REPO,
15543
+ gitDerivedSlug
15544
+ );
15545
+ const octokit = new Octokit2({ auth: githubToken });
15546
+ const issues = await octokit.paginate(
15547
+ octokit.rest.issues.listForRepo,
15548
+ {
15549
+ owner,
15550
+ repo: repo2,
15551
+ state: listState,
15552
+ per_page: 100
15553
+ }
15554
+ );
15555
+ const { candidates, skipped } = partitionGithubIssuesForImport({
15556
+ projection: proj,
15557
+ issues,
15558
+ onlyIssueNumbers
15559
+ });
15560
+ if (o.dryRun) {
15561
+ deps.log(
15562
+ formatOutput(g.format, {
15563
+ ok: true,
15564
+ dryRun: true,
15565
+ candidates,
15566
+ skipped
15567
+ })
15568
+ );
15569
+ } else {
15570
+ const imported = [];
15571
+ for (const c of candidates) {
15572
+ const ticketId = ulid();
15573
+ const createPayload = mergeTicketImportCreatePayload(
15574
+ ticketId,
15575
+ c.ticketCreatedPayloadBase,
15576
+ storyTrimmed
15577
+ );
15578
+ const createdEvt = makeEvent(
15579
+ "TicketCreated",
15580
+ createPayload,
15581
+ deps.clock,
15582
+ actor
15583
+ );
15584
+ await appendEventLine(
15585
+ session.worktreePath,
15586
+ createdEvt,
15587
+ deps.clock
15588
+ );
15589
+ const linkEvt = makeEvent(
15590
+ "GithubIssueLinked",
15591
+ { ticketId, issueNumber: c.issueNumber },
15592
+ deps.clock,
15593
+ actor
15594
+ );
15595
+ await appendEventLine(session.worktreePath, linkEvt, deps.clock);
15596
+ imported.push({ ticketId, issueNumber: c.issueNumber });
15597
+ }
15598
+ await commitDataWorktreeIfNeeded(
15599
+ session.worktreePath,
15600
+ formatDataBranchCommitMessage(
15601
+ "hyper-pm: import-github-issues",
15602
+ actor
15603
+ ),
15604
+ runGit
15605
+ );
15606
+ deps.log(
15607
+ formatOutput(g.format, {
15608
+ ok: true,
15609
+ imported,
15610
+ skipped
15611
+ })
15612
+ );
15613
+ }
15614
+ } catch (e) {
15615
+ deps.error(e instanceof Error ? e.message : String(e));
15616
+ deps.exit(ExitCode.ExternalApi);
15617
+ }
15618
+ } finally {
15619
+ await session.dispose();
15620
+ }
15621
+ deps.exit(ExitCode.Success);
15622
+ });
15623
+ program2.command("sync").description("GitHub Issues sync").option("--no-github", "skip network sync", false).option(
15624
+ "--skip-push",
15625
+ "after a successful sync, do not push the data branch to the remote",
15626
+ false
15627
+ ).action(async function() {
15232
15628
  const g = readGlobals(this);
15233
15629
  const o = this.opts();
15234
15630
  const repoRoot = await resolveRepoRoot(g.repo);
@@ -15310,7 +15706,33 @@ ${body}`
15310
15706
  formatDataBranchCommitMessage("hyper-pm: sync", outboundActor),
15311
15707
  runGit
15312
15708
  );
15313
- deps.log(formatOutput(g.format, { ok: true }));
15709
+ let dataBranchPush;
15710
+ let dataBranchPushDetail;
15711
+ if (o.skipPush) {
15712
+ dataBranchPush = "skipped_cli";
15713
+ dataBranchPushDetail = "skip-push";
15714
+ } else {
15715
+ const pushResult = await tryPushDataBranchToRemote(
15716
+ session.worktreePath,
15717
+ cfg.remote,
15718
+ cfg.dataBranch,
15719
+ runGit
15720
+ );
15721
+ dataBranchPush = pushResult.status;
15722
+ dataBranchPushDetail = pushResult.detail;
15723
+ if (pushResult.status === "failed" && pushResult.detail) {
15724
+ deps.error(
15725
+ `hyper-pm: data branch not pushed (${cfg.remote}/${cfg.dataBranch}): ${pushResult.detail}`
15726
+ );
15727
+ }
15728
+ }
15729
+ deps.log(
15730
+ formatOutput(g.format, {
15731
+ ok: true,
15732
+ dataBranchPush,
15733
+ ...dataBranchPushDetail !== void 0 ? { dataBranchPushDetail } : {}
15734
+ })
15735
+ );
15314
15736
  } catch (e) {
15315
15737
  deps.error(e instanceof Error ? e.message : String(e));
15316
15738
  deps.exit(ExitCode.ExternalApi);