@kw-yeh/vibeflow 4.9.0 → 4.10.0

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/vibeflow.mjs CHANGED
@@ -1103,7 +1103,7 @@ import fs22 from "fs";
1103
1103
  import os9 from "os";
1104
1104
  import path26 from "path";
1105
1105
  import React, { useEffect, useRef, useState } from "react";
1106
- import { Box, Text, render, useApp, useInput } from "ink";
1106
+ import { Box, Text, render as render2, useApp, useInput } from "ink";
1107
1107
  function newFormState(defaultAutoMode, agents) {
1108
1108
  const agentCli = agents && agents.length > 0 && !agents.some((a) => a.id === "claude") ? agents[0].id : "claude";
1109
1109
  return {
@@ -1477,7 +1477,7 @@ function FormView({ api, ctx, initial, initialFocus, initialMessage, pristine, o
1477
1477
  function showForm(api, ctx, initial, pristine, initialFocus = 0, initialMessage) {
1478
1478
  return new Promise((resolve) => {
1479
1479
  let result = { type: "cancel" };
1480
- const instance = render(
1480
+ const instance = render2(
1481
1481
  h(FormView, { api, ctx, initial, initialFocus, initialMessage, pristine, onResult: (r) => result = r }),
1482
1482
  { exitOnCtrlC: false }
1483
1483
  );
@@ -1560,7 +1560,7 @@ __export(src_exports, {
1560
1560
  import { spawnSync as spawnSync2 } from "child_process";
1561
1561
  import path27 from "path";
1562
1562
  import React2, { useEffect as useEffect2, useState as useState2 } from "react";
1563
- import { Box as Box2, Text as Text2, render as render2, useApp as useApp2, useInput as useInput2, useStdout } from "ink";
1563
+ import { Box as Box2, Text as Text2, render as render3, useApp as useApp2, useInput as useInput2, useStdout } from "ink";
1564
1564
  function findDetachKey(text) {
1565
1565
  const plain = text.indexOf(DETACH_KEY);
1566
1566
  const m = /\x1b\[\d+;\d+;29;1;\d+;\d+_/.exec(text);
@@ -1828,7 +1828,7 @@ function BoardView({ opts, initial, initialMessage, onAction }) {
1828
1828
  function showBoard(opts, initial, initialMessage) {
1829
1829
  return new Promise((resolve) => {
1830
1830
  let result = { action: { type: "quit" }, sel: initial };
1831
- const instance = render2(
1831
+ const instance = render3(
1832
1832
  h2(BoardView, {
1833
1833
  opts,
1834
1834
  initial,
@@ -2464,6 +2464,13 @@ function createBridge(t) {
2464
2464
  logoutGithubAuth: () => t.invoke("settings:logoutGithubAuth"),
2465
2465
  /** Open Issues/PRs of the board's GitHub repos that involve me; cached unless `force`. */
2466
2466
  getGithubInbox: (opts) => t.invoke("github:inbox", opts ?? {}),
2467
+ getJiraAuthStatus: () => t.invoke("jira:authStatus"),
2468
+ startJiraAuthLogin: () => t.invoke("settings:startJiraAuthLogin"),
2469
+ cancelJiraAuthLogin: () => t.invoke("settings:cancelJiraAuthLogin"),
2470
+ inputJiraAuthLogin: (input) => t.invoke("settings:inputJiraAuthLogin", input),
2471
+ logoutJiraAuth: () => t.invoke("settings:logoutJiraAuth"),
2472
+ onJiraAuthEvent: (callback) => t.on("jira-auth:event", (event) => callback(event)),
2473
+ getJiraInbox: (opts) => t.invoke("jira:inbox", opts ?? {}),
2467
2474
  /** The Issue/PR each card belongs to, keyed by task id. */
2468
2475
  getGithubTaskLinks: (opts) => t.invoke("github:taskLinks", opts ?? {}),
2469
2476
  onGithubAuthEvent: (callback) => {
@@ -2963,10 +2970,11 @@ function parseGithubRepo(remoteUrl) {
2963
2970
  return match ? `${match[1]}/${match[2]}` : null;
2964
2971
  }
2965
2972
  var REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
2973
+ var isRepoName = (repo) => REPO_RE.test(repo) && repo.split("/").every((part) => part !== "." && part !== "..");
2966
2974
  function isGithubRef(value) {
2967
2975
  if (!value || typeof value !== "object") return false;
2968
2976
  const v = value;
2969
- return (v.kind === "issue" || v.kind === "pr") && typeof v.repo === "string" && REPO_RE.test(v.repo) && typeof v.number === "number" && Number.isInteger(v.number) && v.number > 0 && typeof v.url === "string" && v.url.startsWith(`https://github.com/${v.repo}/`);
2977
+ return (v.kind === "issue" || v.kind === "pr") && typeof v.repo === "string" && isRepoName(v.repo) && typeof v.number === "number" && Number.isInteger(v.number) && v.number > 0 && typeof v.url === "string" && v.url.startsWith(`https://github.com/${v.repo}/`);
2970
2978
  }
2971
2979
  var ITEM_FIELDS = "number,title,url,body,author,assignees,labels,milestone,createdAt";
2972
2980
  var ISSUE_FIELDS = `${ITEM_FIELDS},issueType`;
@@ -2974,6 +2982,7 @@ var PR_FIELDS = `${ITEM_FIELDS},state,isDraft,reviewDecision,headRefName,baseRef
2974
2982
  var PR_INDEX_FIELDS = "number,url,state,isDraft,headRefName,closingIssuesReferences";
2975
2983
  var LIST_LIMIT = "50";
2976
2984
  var PR_INDEX_LIMIT = "100";
2985
+ var SEARCH_LIMIT = "1000";
2977
2986
  async function runJson(run3, args) {
2978
2987
  const out = await run3(args);
2979
2988
  const parsed = JSON.parse(out || "[]");
@@ -3070,6 +3079,34 @@ async function fetchRepoItems(run3, repo) {
3070
3079
  prs: mergeItems([assignedPrs, authoredPrs, reviewPrs, reviewedPrs])
3071
3080
  };
3072
3081
  }
3082
+ async function findRelatedRepos(run3) {
3083
+ const searches = [
3084
+ ["issues", ["--assignee", "@me"]],
3085
+ ["issues", ["--author", "@me"]],
3086
+ ["prs", ["--assignee", "@me"]],
3087
+ ["prs", ["--author", "@me"]],
3088
+ ["prs", ["--review-requested", "@me"]],
3089
+ ["prs", ["--reviewed-by", "@me"]]
3090
+ ];
3091
+ const results = await Promise.all(searches.map(([kind, filter]) => runJson(run3, [
3092
+ "search",
3093
+ kind,
3094
+ "--state",
3095
+ "open",
3096
+ "--limit",
3097
+ SEARCH_LIMIT,
3098
+ ...filter,
3099
+ "--json",
3100
+ "repository"
3101
+ ])));
3102
+ const found2 = /* @__PURE__ */ new Set();
3103
+ for (const rows of results) for (const row of rows) {
3104
+ const repository = row.repository;
3105
+ const name = repository?.nameWithOwner;
3106
+ if (typeof name === "string" && isRepoName(name)) found2.add(name);
3107
+ }
3108
+ return Array.from(found2).sort();
3109
+ }
3073
3110
  async function fetchPrIndex(run3, repo) {
3074
3111
  const rows = await runJson(run3, [
3075
3112
  "pr",
@@ -3166,6 +3203,7 @@ function createGithubService(options) {
3166
3203
  const items = /* @__PURE__ */ new Map();
3167
3204
  const indexes = /* @__PURE__ */ new Map();
3168
3205
  const repos = /* @__PURE__ */ new Map();
3206
+ const discovery = /* @__PURE__ */ new Map();
3169
3207
  function cached2(map, key, force, load) {
3170
3208
  const hit = map.get(key);
3171
3209
  if (hit && !force && now() - hit.at < GITHUB_CACHE_TTL_MS) return hit;
@@ -3196,11 +3234,12 @@ function createGithubService(options) {
3196
3234
  const byProject = await projectRepos(board, force);
3197
3235
  const repoProject = /* @__PURE__ */ new Map();
3198
3236
  for (const [project, repo] of byProject) if (!repoProject.has(repo)) repoProject.set(repo, project);
3199
- const entries = Array.from(repoProject.entries());
3237
+ const discovered = cached2(discovery, "related", force, () => findRelatedRepos(run3));
3238
+ const entries = (await discovered.value).map((repo) => [repo, repoProject.get(repo) ?? null]);
3200
3239
  const results = await Promise.all(
3201
3240
  entries.map(async ([repo, projectPath]) => {
3202
3241
  const entry = cached2(items, repo, force, () => fetchRepoItems(run3, repo));
3203
- const head = { repo, projectPath, projectName: basename(projectPath) };
3242
+ const head = { repo, projectPath, projectName: projectPath ? basename(projectPath) : repo };
3204
3243
  try {
3205
3244
  return { at: entry.at, inbox: { ...head, ...await entry.value } };
3206
3245
  } catch (err) {
@@ -3211,7 +3250,7 @@ function createGithubService(options) {
3211
3250
  return {
3212
3251
  status: "ok",
3213
3252
  ...auth.login ? { login: auth.login } : {},
3214
- fetchedAt: results.length ? Math.min(...results.map((r) => r.at)) : 0,
3253
+ fetchedAt: Math.min(discovered.at, ...results.map((r) => r.at)),
3215
3254
  repos: results.map((r) => r.inbox).sort((a, b) => a.repo.localeCompare(b.repo))
3216
3255
  };
3217
3256
  },
@@ -3654,10 +3693,10 @@ async function ignoredEntryFitsCopyBudget(src) {
3654
3693
  const stat = await fs8.lstat(current);
3655
3694
  if (stat.isSymbolicLink()) continue;
3656
3695
  if (stat.isDirectory()) {
3657
- const children = await fs8.readdir(current, { withFileTypes: true });
3658
- files += children.length;
3696
+ const children2 = await fs8.readdir(current, { withFileTypes: true });
3697
+ files += children2.length;
3659
3698
  if (files > MAX_IGNORED_COPY_FILES) return false;
3660
- for (const child of children) stack.push(path12.join(current, child.name));
3699
+ for (const child of children2) stack.push(path12.join(current, child.name));
3661
3700
  continue;
3662
3701
  }
3663
3702
  files += 1;
@@ -4305,7 +4344,8 @@ async function createTaskFromInput(input) {
4305
4344
  model: input.model || void 0,
4306
4345
  effort: input.effort ?? DEFAULT_TASK_EFFORT,
4307
4346
  autoMode: input.autoMode,
4308
- ...input.github ? { github: input.github } : {}
4347
+ ...input.github ? { github: input.github } : {},
4348
+ ...input.jira ? { jira: input.jira } : {}
4309
4349
  };
4310
4350
  try {
4311
4351
  const board = store.get("board");
@@ -4789,17 +4829,17 @@ function listLibrary(root, builtinDir) {
4789
4829
  const full = path14.join(dir, name);
4790
4830
  if (wantsDirectory && !fs10.existsSync(path14.join(full, "SKILL.md"))) continue;
4791
4831
  const key = entryKey(kind, name);
4792
- const record = index.entries[key] ?? {};
4832
+ const record2 = index.entries[key] ?? {};
4793
4833
  entries.push({
4794
4834
  key,
4795
4835
  kind,
4796
4836
  name,
4797
4837
  path: full,
4798
- description: wantsDirectory ? readSkillDescription(path14.join(full, "SKILL.md")) : record.description,
4799
- sourcePath: record.sourcePath,
4800
- importedAt: record.importedAt,
4801
- enabled: record.enabled !== false,
4802
- builtin: wantsDirectory && record.builtinHash && builtinDir !== void 0 ? builtinStatus(full, name, record.builtinHash, builtinDir) : void 0
4838
+ description: wantsDirectory ? readSkillDescription(path14.join(full, "SKILL.md")) : record2.description,
4839
+ sourcePath: record2.sourcePath,
4840
+ importedAt: record2.importedAt,
4841
+ enabled: record2.enabled !== false,
4842
+ builtin: wantsDirectory && record2.builtinHash && builtinDir !== void 0 ? builtinStatus(full, name, record2.builtinHash, builtinDir) : void 0
4803
4843
  });
4804
4844
  }
4805
4845
  }
@@ -4874,8 +4914,8 @@ function markBuiltinDeleted(root, key) {
4874
4914
  function deleteEntry(root, kind, name) {
4875
4915
  fs10.rmSync(entryPath(root, kind, name), { recursive: true, force: true });
4876
4916
  const key = entryKey(kind, name);
4877
- const record = readIndex(root).entries[key];
4878
- if (record?.builtinHash || record?.builtinDeleted) {
4917
+ const record2 = readIndex(root).entries[key];
4918
+ if (record2?.builtinHash || record2?.builtinDeleted) {
4879
4919
  markBuiltinDeleted(root, key);
4880
4920
  return;
4881
4921
  }
@@ -5070,10 +5110,10 @@ function syncBuiltinSkills(root, builtinDir) {
5070
5110
  }
5071
5111
  function syncOne(root, builtinDir, name, result) {
5072
5112
  const key = entryKey("skill", name);
5073
- const record = readIndex(root).entries[key] ?? {};
5074
- if (record.builtinDeleted) return;
5113
+ const record2 = readIndex(root).entries[key] ?? {};
5114
+ if (record2.builtinDeleted) return;
5075
5115
  const target = entryPath(root, "skill", name);
5076
- if (!record.builtinHash) {
5116
+ if (!record2.builtinHash) {
5077
5117
  if (fs11.existsSync(target)) return;
5078
5118
  annotate(root, key, {
5079
5119
  builtinHash: installBuiltin(root, builtinDir, name),
@@ -5087,8 +5127,8 @@ function syncOne(root, builtinDir, name, result) {
5087
5127
  markBuiltinDeleted(root, key);
5088
5128
  return;
5089
5129
  }
5090
- if (hashSkillDir(path15.join(builtinDir, name)) === record.builtinHash) return;
5091
- if (hashSkillDir(target) !== record.builtinHash) return;
5130
+ if (hashSkillDir(path15.join(builtinDir, name)) === record2.builtinHash) return;
5131
+ if (hashSkillDir(target) !== record2.builtinHash) return;
5092
5132
  annotate(root, key, { builtinHash: installBuiltin(root, builtinDir, name) });
5093
5133
  result.updated.push(name);
5094
5134
  }
@@ -5103,10 +5143,10 @@ function restoreBuiltinSkill(root, builtinDir, name) {
5103
5143
  ensureLibrary(root);
5104
5144
  const builtinHash = installBuiltin(root, builtinDir, name);
5105
5145
  const index = readIndex(root);
5106
- const record = { ...index.entries[key], builtinHash, enabled: true, importedAt: Date.now() };
5107
- delete record.builtinDeleted;
5108
- delete record.sourcePath;
5109
- index.entries[key] = record;
5146
+ const record2 = { ...index.entries[key], builtinHash, enabled: true, importedAt: Date.now() };
5147
+ delete record2.builtinDeleted;
5148
+ delete record2.sourcePath;
5149
+ index.entries[key] = record2;
5110
5150
  writeIndex(root, index);
5111
5151
  const entry = listLibrary(root, builtinDir).find((e) => e.key === key);
5112
5152
  if (!entry) throw new Error(`\u9084\u539F\u5931\u6557\uFF1A${name}`);
@@ -5203,12 +5243,12 @@ function spawnJsonLines(bin, args, opts) {
5203
5243
  }
5204
5244
  });
5205
5245
  proc.on("exit", (code2, signal) => {
5206
- finished();
5207
5246
  clearTimeout(timer);
5208
5247
  clearTimeout(graceTimer);
5209
5248
  const reason = signal ? `was killed (${signal})` : `exited with code ${code2}`;
5210
5249
  fail2(new Error(`${bin} ${reason}${stderr.trim() ? `: ${stderr.trim()}` : ""}`));
5211
5250
  });
5251
+ proc.on("close", finished);
5212
5252
  proc.stdin.on("error", () => {
5213
5253
  });
5214
5254
  const kill = () => {
@@ -5644,6 +5684,334 @@ function startGitHubCliLogin(onEvent) {
5644
5684
  });
5645
5685
  }
5646
5686
 
5687
+ // packages/core/src/jira-adf.ts
5688
+ function record(value) {
5689
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
5690
+ }
5691
+ function children(node) {
5692
+ return Array.isArray(node.content) ? node.content : [];
5693
+ }
5694
+ function render(node, depth = 0) {
5695
+ if (typeof node === "string") return node;
5696
+ const item = record(node);
5697
+ if (!item) return "";
5698
+ const type = item.type;
5699
+ const body = () => children(item).map((child) => render(child, depth)).join("");
5700
+ const attrs = record(item.attrs);
5701
+ if (type === "text") {
5702
+ let value = typeof item.text === "string" ? item.text : "";
5703
+ for (const mark of Array.isArray(item.marks) ? item.marks : []) {
5704
+ const m = record(mark);
5705
+ if (m?.type === "strong") value = `**${value}**`;
5706
+ if (m?.type === "em") value = `*${value}*`;
5707
+ if (m?.type === "strike") value = `~~${value}~~`;
5708
+ if (m?.type === "code") value = `\`${value}\``;
5709
+ if (m?.type === "link" && typeof record(m.attrs)?.href === "string") value = `[${value}](${record(m.attrs)?.href})`;
5710
+ }
5711
+ return value;
5712
+ }
5713
+ if (type === "hardBreak") return " \n";
5714
+ if (type === "rule") return "\n---\n\n";
5715
+ if (type === "mention") return `@${String(attrs?.text ?? attrs?.id ?? "")}`;
5716
+ if (type === "emoji") return String(attrs?.shortName ?? "");
5717
+ if (type === "inlineCard" || type === "blockCard") return `[${String(attrs?.url ?? "\u9023\u7D50")}](${String(attrs?.url ?? "")})`;
5718
+ if (type === "media" || type === "mediaSingle") return "[\u9644\u4EF6]";
5719
+ if (type === "paragraph") return `${body()}
5720
+
5721
+ `;
5722
+ if (type === "heading") return `${"#".repeat(Math.min(6, Math.max(1, Number(attrs?.level) || 1)))} ${body()}
5723
+
5724
+ `;
5725
+ if (type === "blockquote") return body().trim().split("\n").map((line) => `> ${line}`).join("\n") + "\n\n";
5726
+ if (type === "codeBlock") return `
5727
+ \`\`\`${String(attrs?.language ?? "")}
5728
+ ${children(item).map((child) => render(child)).join("").trimEnd()}
5729
+ \`\`\`
5730
+
5731
+ `;
5732
+ if (type === "bulletList" || type === "orderedList") {
5733
+ return children(item).map((child, index) => {
5734
+ const content = render(child, depth + 1).trim().replace(/\n\n/g, "\n");
5735
+ const prefix = type === "orderedList" ? `${index + 1}. ` : "- ";
5736
+ return `${" ".repeat(depth)}${prefix}${content}
5737
+ `;
5738
+ }).join("") + "\n";
5739
+ }
5740
+ if (type === "listItem") return body();
5741
+ if (type === "table") {
5742
+ const rows = children(item).map((row) => {
5743
+ const cells = children(record(row) ?? {}).map((cell) => render(cell).trim().replace(/\|/g, "\\|").replace(/\n+/g, " "));
5744
+ return `| ${cells.join(" | ")} |`;
5745
+ });
5746
+ if (rows.length === 0) return "";
5747
+ const width = children(record(children(item)[0]) ?? {}).length;
5748
+ rows.splice(1, 0, `| ${Array(width).fill("---").join(" | ")} |`);
5749
+ return rows.join("\n") + "\n\n";
5750
+ }
5751
+ return body();
5752
+ }
5753
+ function adfToMarkdown(value) {
5754
+ return render(value).trim();
5755
+ }
5756
+
5757
+ // packages/core/src/jira-auth.ts
5758
+ init_env();
5759
+ import { execFile as execFile8 } from "child_process";
5760
+ import { promisify as promisify5 } from "util";
5761
+ import * as nodePty4 from "node-pty";
5762
+ var pexec5 = promisify5(execFile8);
5763
+ var activeLogin2 = null;
5764
+ var activeCancel = null;
5765
+ function stripAnsi2(value) {
5766
+ return value.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "").replace(/\x07/g, "");
5767
+ }
5768
+ function hasSingleSitePrompt(output) {
5769
+ const clean = stripAnsi2(output);
5770
+ if (!/select the site to login/i.test(clean) || !/enter submit/i.test(clean)) return false;
5771
+ const sites = new Set([...clean.matchAll(/https:\/\/([a-z0-9.-]+\.atlassian\.net)\b/gi)].map((match) => match[1].toLowerCase()));
5772
+ return sites.size === 1;
5773
+ }
5774
+ function parseAuthStatus(output) {
5775
+ const clean = stripAnsi2(output);
5776
+ const site = clean.match(/(?:site|host)\s*:\s*(?:https?:\/\/)?([a-z0-9.-]+\.atlassian\.net)/i)?.[1];
5777
+ const email = clean.match(/(?:email|user)\s*:\s*([^\s]+@[^\s]+)/i)?.[1];
5778
+ return {
5779
+ installed: true,
5780
+ authenticated: Boolean(site) && !/not (?:logged|authenticated)|unauthenticated/i.test(clean),
5781
+ ...site ? { site } : {},
5782
+ ...email ? { email } : {}
5783
+ };
5784
+ }
5785
+ var defaultAcliRunner = async (args) => {
5786
+ const { stdout } = await pexec5("acli", args, {
5787
+ env: execEnv(),
5788
+ timeout: 6e4,
5789
+ maxBuffer: 32 * 1024 * 1024,
5790
+ windowsHide: true
5791
+ });
5792
+ return stdout.toString();
5793
+ };
5794
+ async function getJiraAuthStatus(run3 = defaultAcliRunner) {
5795
+ try {
5796
+ return parseAuthStatus(await run3(["jira", "auth", "status"]));
5797
+ } catch (err) {
5798
+ const error = err;
5799
+ if (error.code === "ENOENT") return { installed: false, authenticated: false };
5800
+ return { installed: true, authenticated: false, error: stripAnsi2(String(error.stderr || error.message || err)) };
5801
+ }
5802
+ }
5803
+ function cancelJiraLogin() {
5804
+ activeCancel?.();
5805
+ activeCancel = null;
5806
+ if (!activeLogin2) return;
5807
+ const process2 = activeLogin2;
5808
+ activeLogin2 = null;
5809
+ try {
5810
+ process2.kill();
5811
+ } catch {
5812
+ }
5813
+ }
5814
+ function inputJiraLogin(data) {
5815
+ activeLogin2?.write(data);
5816
+ }
5817
+ function startJiraLogin(onEvent, run3 = defaultAcliRunner) {
5818
+ cancelJiraLogin();
5819
+ onEvent({ type: "starting" });
5820
+ let process2;
5821
+ try {
5822
+ process2 = nodePty4.spawn("acli", ["jira", "auth", "login", "--web"], {
5823
+ name: "xterm-256color",
5824
+ cwd: processCwd(),
5825
+ env: { ...execEnv(), TERM: "xterm-256color" },
5826
+ cols: 100,
5827
+ rows: 24
5828
+ });
5829
+ } catch (err) {
5830
+ onEvent({ type: "error", error: String(err.message ?? err) });
5831
+ return;
5832
+ }
5833
+ activeLogin2 = process2;
5834
+ let output = "";
5835
+ let sentUrl = false;
5836
+ let autoSelectedSite = false;
5837
+ let cancelled = false;
5838
+ activeCancel = () => {
5839
+ cancelled = true;
5840
+ onEvent({ type: "cancelled" });
5841
+ };
5842
+ process2.onData((chunk) => {
5843
+ onEvent({ type: "output", data: chunk });
5844
+ output += chunk;
5845
+ if (output.length > 64e3) output = output.slice(-64e3);
5846
+ const clean = stripAnsi2(output);
5847
+ const url = clean.match(/https:\/\/[^\s<>"']+/)?.[0];
5848
+ if (url && !sentUrl) {
5849
+ sentUrl = true;
5850
+ onEvent({ type: "url", url });
5851
+ }
5852
+ if (!autoSelectedSite && hasSingleSitePrompt(clean)) {
5853
+ autoSelectedSite = true;
5854
+ process2.write("\r");
5855
+ }
5856
+ });
5857
+ process2.onExit(async ({ exitCode }) => {
5858
+ if (activeLogin2 !== process2) return;
5859
+ activeLogin2 = null;
5860
+ activeCancel = null;
5861
+ if (cancelled) return;
5862
+ if (exitCode === 0) {
5863
+ const status = await getJiraAuthStatus(run3);
5864
+ onEvent(status.authenticated ? { type: "success", status } : { type: "error", error: status.error ?? "\u767B\u5165\u5B8C\u6210\uFF0C\u4F46\u7121\u6CD5\u78BA\u8A8D Atlassian \u5E33\u865F\u3002" });
5865
+ } else {
5866
+ const clean = stripAnsi2(output);
5867
+ const error = clean.match(/(?:✗\s*)?Error:[^\r\n]*/g)?.at(-1) ?? clean.split(/[\r\n]+/).map((line) => line.trim()).filter(Boolean).at(-1);
5868
+ onEvent({ type: "error", error: error || `Atlassian \u767B\u5165\u5931\u6557\uFF08exit ${exitCode}\uFF09\u3002` });
5869
+ }
5870
+ });
5871
+ }
5872
+ function processCwd() {
5873
+ return process.cwd();
5874
+ }
5875
+ async function logoutJira(run3 = defaultAcliRunner) {
5876
+ await run3(["jira", "auth", "logout"]);
5877
+ return getJiraAuthStatus(run3);
5878
+ }
5879
+
5880
+ // packages/core/src/jira.ts
5881
+ var asRaw = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
5882
+ var nameOf = (v) => typeof v === "string" ? v : String(asRaw(v).name ?? "");
5883
+ var JIRA_INBOX_JQL = "assignee = currentUser() AND (statusCategory != Done OR sprint in openSprints()) ORDER BY updated DESC";
5884
+ var DEFAULT_POINT_FIELDS = ["customfield_10033", "customfield_10016"];
5885
+ var JIRA_CACHE_TTL_MS = 5 * 60 * 1e3;
5886
+ function buildSearchArgs() {
5887
+ return [
5888
+ "jira",
5889
+ "workitem",
5890
+ "search",
5891
+ "--jql",
5892
+ JIRA_INBOX_JQL,
5893
+ "--fields",
5894
+ "key,summary,status,issuetype,priority,description",
5895
+ "--json",
5896
+ "--paginate",
5897
+ "--limit",
5898
+ "200"
5899
+ ];
5900
+ }
5901
+ function buildViewArgs(key, pointFields = DEFAULT_POINT_FIELDS) {
5902
+ return [
5903
+ "jira",
5904
+ "workitem",
5905
+ "view",
5906
+ key,
5907
+ "--fields",
5908
+ [.../* @__PURE__ */ new Set(["duedate", "parent", "created", "updated", "project", "customfield_10020", ...pointFields])].join(","),
5909
+ "--json"
5910
+ ];
5911
+ }
5912
+ function isJiraRef(value) {
5913
+ const v = asRaw(value);
5914
+ return typeof v.key === "string" && /^[A-Z][A-Z0-9_]+-\d+$/.test(v.key) && typeof v.site === "string" && /^[a-z0-9-]+(?:\.[a-z0-9-]+)*\.atlassian\.net$/.test(v.site) && v.url === `https://${v.site}/browse/${v.key}`;
5915
+ }
5916
+ function toTicket(raw, site, pointFields = DEFAULT_POINT_FIELDS) {
5917
+ const row = asRaw(raw);
5918
+ const fields = asRaw(row.fields);
5919
+ const source = Object.keys(fields).length ? fields : row;
5920
+ const key = row.key;
5921
+ if (typeof key !== "string" || !/^[A-Z][A-Z0-9_]+-\d+$/.test(key)) return null;
5922
+ const status = asRaw(source.status);
5923
+ const category = asRaw(status.statusCategory);
5924
+ const categoryKey = String(category.key ?? category.name ?? "").toLowerCase();
5925
+ const points = pointFields.map((field) => source[field]).find((v) => typeof v === "number" && Number.isFinite(v));
5926
+ const sprintValue = source.customfield_10020;
5927
+ const sprintList = Array.isArray(sprintValue) ? sprintValue : sprintValue ? [sprintValue] : [];
5928
+ const activeSprint = sprintList.map(asRaw).find((s) => String(s.state ?? "").toLowerCase() === "active");
5929
+ const parent = asRaw(source.parent);
5930
+ return {
5931
+ key,
5932
+ url: `https://${site}/browse/${key}`,
5933
+ projectKey: String(asRaw(source.project).key ?? key.split("-")[0]),
5934
+ summary: String(source.summary ?? ""),
5935
+ status: { name: nameOf(source.status), category: categoryKey === "done" ? "done" : categoryKey === "indeterminate" || categoryKey === "in progress" ? "indeterminate" : "new" },
5936
+ issueType: nameOf(source.issuetype),
5937
+ priority: nameOf(source.priority) || null,
5938
+ storyPoints: typeof points === "number" ? points : null,
5939
+ dueDate: typeof source.duedate === "string" ? source.duedate : null,
5940
+ sprint: typeof activeSprint?.name === "string" ? activeSprint.name : null,
5941
+ parentKey: typeof parent.key === "string" ? parent.key : null,
5942
+ description: adfToMarkdown(source.description),
5943
+ createdAt: String(source.created ?? ""),
5944
+ updatedAt: String(source.updated ?? "")
5945
+ };
5946
+ }
5947
+ function searchRows(output) {
5948
+ const parsed = JSON.parse(output);
5949
+ if (Array.isArray(parsed)) return parsed;
5950
+ const body = asRaw(parsed);
5951
+ if (Array.isArray(body.issues)) return body.issues;
5952
+ if (Array.isArray(body.values)) return body.values;
5953
+ throw new Error("\u7121\u6CD5\u8FA8\u8B58 acli Jira search \u7684 JSON \u683C\u5F0F");
5954
+ }
5955
+ async function enrichRows(rows, run3, pointFields) {
5956
+ const enriched = Array.from(rows);
5957
+ let next = 0;
5958
+ let failed = 0;
5959
+ async function worker() {
5960
+ while (next < rows.length) {
5961
+ const index = next++;
5962
+ const row = asRaw(rows[index]);
5963
+ if (typeof row.key !== "string") continue;
5964
+ try {
5965
+ const detail = asRaw(JSON.parse(await run3(buildViewArgs(row.key, pointFields))));
5966
+ enriched[index] = { ...row, fields: { ...asRaw(row.fields), ...asRaw(detail.fields) } };
5967
+ } catch {
5968
+ failed++;
5969
+ }
5970
+ }
5971
+ }
5972
+ await Promise.all(Array.from({ length: Math.min(6, rows.length) }, () => worker()));
5973
+ return { rows: enriched, failed };
5974
+ }
5975
+ function createJiraService(options) {
5976
+ const run3 = options.run ?? defaultAcliRunner;
5977
+ const authStatus = options.authStatus ?? (() => getJiraAuthStatus(run3));
5978
+ const now = options.now ?? Date.now;
5979
+ let cache = null;
5980
+ return {
5981
+ clear() {
5982
+ cache = null;
5983
+ },
5984
+ async inbox(opts = {}) {
5985
+ const auth = await authStatus();
5986
+ if (!auth.installed) return { status: "acli-missing", fetchedAt: 0, tickets: [] };
5987
+ if (!auth.authenticated || !auth.site) return { status: "unauthenticated", fetchedAt: 0, tickets: [] };
5988
+ if (cache && cache.site === auth.site && !opts.force && now() - cache.at < JIRA_CACHE_TTL_MS) return cache.value;
5989
+ const at = now();
5990
+ const pointFields = options.pointFields?.() ?? DEFAULT_POINT_FIELDS;
5991
+ const value = run3(buildSearchArgs()).then(async (output) => {
5992
+ const detail = await enrichRows(searchRows(output), run3, pointFields);
5993
+ return {
5994
+ status: "ok",
5995
+ site: auth.site,
5996
+ email: auth.email,
5997
+ fetchedAt: at,
5998
+ tickets: detail.rows.map((row) => toTicket(row, auth.site, pointFields)).filter((ticket) => ticket !== null),
5999
+ ...detail.failed ? { error: `${detail.failed} \u5F35 Jira ticket \u7684\u8A73\u7D30\u6B04\u4F4D\u8B80\u53D6\u5931\u6557\uFF1B\u6E05\u55AE\u4ECD\u53EF\u67E5\u770B\u3002` } : {}
6000
+ };
6001
+ }).catch((err) => ({
6002
+ status: "ok",
6003
+ site: auth.site,
6004
+ email: auth.email,
6005
+ fetchedAt: at,
6006
+ tickets: [],
6007
+ error: String(err.stderr || err.message || err)
6008
+ }));
6009
+ cache = { at, site: auth.site, value };
6010
+ return value;
6011
+ }
6012
+ };
6013
+ }
6014
+
5647
6015
  // packages/core/src/board-cli.ts
5648
6016
  import fs14 from "fs";
5649
6017
  import path18 from "path";
@@ -5766,7 +6134,7 @@ async function readSpecsAt(cwd, ref, paths) {
5766
6134
  // packages/core/src/chat-session.ts
5767
6135
  init_env();
5768
6136
  init_git_bash();
5769
- import { spawn as spawn6 } from "child_process";
6137
+ import { spawn as spawn7 } from "child_process";
5770
6138
  import { randomUUID as randomUUID3 } from "crypto";
5771
6139
 
5772
6140
  // packages/core/src/chat-store.ts
@@ -5922,7 +6290,7 @@ ${attachmentLines.join("\n")}` : text;
5922
6290
  `--append-system-prompt ${quote(systemPrompt)}`,
5923
6291
  quote(fullText)
5924
6292
  ].filter(Boolean).join(" ") : buildRawAgentCommand(agentCli, model, fullText, systemPrompt, worktreePath, quote, workspacePath);
5925
- const proc = spawn6(shell.command, shell.args(cmd), {
6293
+ const proc = spawn7(shell.command, shell.args(cmd), {
5926
6294
  cwd: worktreePath,
5927
6295
  env: execEnv()
5928
6296
  });
@@ -6397,7 +6765,7 @@ function validBoard(value) {
6397
6765
  }
6398
6766
  return next;
6399
6767
  }
6400
- function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatus, modelCatalog }) {
6768
+ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatus, acliRunner, jiraAuthStatus, modelCatalog }) {
6401
6769
  const sink = bus.sink();
6402
6770
  const models = modelCatalog ?? createModelCatalog({ version });
6403
6771
  let libraryCodexHome = null;
@@ -6549,6 +6917,11 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
6549
6917
  authStatus: githubAuthStatus ?? getGitHubCliAuthStatus,
6550
6918
  repoOf: async (projectPath) => parseGithubRepo((await getGitInfo(projectPath)).remoteUrl)
6551
6919
  });
6920
+ const jira = createJiraService({
6921
+ run: acliRunner,
6922
+ authStatus: jiraAuthStatus ?? (() => getJiraAuthStatus(acliRunner)),
6923
+ pointFields: () => getSettings().jira?.storyPointsFields?.length ? getSettings().jira.storyPointsFields : DEFAULT_POINT_FIELDS
6924
+ });
6552
6925
  const handlers = {
6553
6926
  "vibeflow:getState": () => getState(),
6554
6927
  // A card's progress right now (todo list, usage, activity), for a frontend
@@ -6568,7 +6941,13 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
6568
6941
  return getState();
6569
6942
  },
6570
6943
  "vibeflow:setSettings": (patch) => {
6571
- setSettings(obj(patch, "patch"));
6944
+ const value = obj(patch, "patch");
6945
+ if (value.jira !== void 0) {
6946
+ const fields = obj(value.jira, "jira")?.storyPointsFields;
6947
+ if (fields !== void 0 && (!Array.isArray(fields) || !fields.every((field) => typeof field === "string" && /^customfield_\d+$/.test(field)))) invalid("jira.storyPointsFields must contain customfield IDs");
6948
+ jira.clear();
6949
+ }
6950
+ setSettings(value);
6572
6951
  return getState();
6573
6952
  },
6574
6953
  "settings:githubAuthStatus": () => getGitHubCliAuthStatus(),
@@ -6589,6 +6968,25 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
6589
6968
  const p = payload === void 0 ? {} : obj(payload, "payload");
6590
6969
  return github.taskLinks(getState().board, { force: p.force === true });
6591
6970
  },
6971
+ "jira:authStatus": () => (jiraAuthStatus ?? (() => getJiraAuthStatus(acliRunner)))(),
6972
+ "settings:startJiraAuthLogin": () => {
6973
+ startJiraLogin((event) => {
6974
+ if (event.type === "success") jira.clear();
6975
+ bus.emit("jira-auth:event", event);
6976
+ }, acliRunner);
6977
+ },
6978
+ "settings:cancelJiraAuthLogin": () => cancelJiraLogin(),
6979
+ "settings:inputJiraAuthLogin": (payload) => inputJiraLogin(str(payload, "input").slice(0, 1e3)),
6980
+ "settings:logoutJiraAuth": async () => {
6981
+ const status = await logoutJira(acliRunner);
6982
+ jira.clear();
6983
+ bus.emit("jira-auth:event", { type: "signed-out", status });
6984
+ return status;
6985
+ },
6986
+ "jira:inbox": (payload) => {
6987
+ const p = payload === void 0 ? {} : obj(payload, "payload");
6988
+ return jira.inbox({ force: p.force === true });
6989
+ },
6592
6990
  "env:detectAgents": () => detectAgents(),
6593
6991
  "agents:listModels": (payload) => {
6594
6992
  const { agentId: rawId, refresh } = obj(payload, "payload");
@@ -6620,6 +7018,7 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
6620
7018
  if (p.effort !== void 0 && !AGENT_EFFORTS.includes(p.effort)) invalid("unknown effort");
6621
7019
  if (p.agentCli !== void 0 && !AGENT_IDS2.includes(p.agentCli)) invalid("unknown agent");
6622
7020
  if (p.github !== void 0 && !isGithubRef(p.github)) invalid("github must be an issue or pr ref on github.com");
7021
+ if (p.jira !== void 0 && !isJiraRef(p.jira)) invalid("jira must be a Jira ticket ref");
6623
7022
  const { task } = await createTaskFromInput({
6624
7023
  projectPath: p.projectPath,
6625
7024
  title: p.title,
@@ -6632,7 +7031,8 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
6632
7031
  effort: p.effort,
6633
7032
  autoMode: p.autoMode,
6634
7033
  attachments: p.attachments,
6635
- github: p.github
7034
+ github: p.github,
7035
+ jira: p.jira
6636
7036
  });
6637
7037
  return { state: getState(), task };
6638
7038
  },
@@ -7049,6 +7449,7 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7049
7449
  models.warm();
7050
7450
  },
7051
7451
  shutdown() {
7452
+ cancelJiraLogin();
7052
7453
  sessions.shutdown();
7053
7454
  cancelAllChatSends();
7054
7455
  unwatchAllSubAgents();
@@ -7505,12 +7906,12 @@ function connectToHost(lock) {
7505
7906
  // packages/cli/src/doctor.ts
7506
7907
  import fs21 from "fs";
7507
7908
  import path25 from "path";
7508
- import { execFile as execFile8 } from "child_process";
7509
- import { promisify as promisify5 } from "util";
7909
+ import { execFile as execFile9 } from "child_process";
7910
+ import { promisify as promisify6 } from "util";
7510
7911
  init_env();
7511
7912
  init_git_bash();
7512
7913
  init_tmux_backend();
7513
- var pexec5 = promisify5(execFile8);
7914
+ var pexec6 = promisify6(execFile9);
7514
7915
  function parseVersion(text) {
7515
7916
  const m = /(\d+)\.(\d+)(?:\.(\d+))?/.exec(text);
7516
7917
  return m ? [Number(m[1]), Number(m[2]), Number(m[3] ?? 0)] : [];
@@ -7524,7 +7925,7 @@ function versionAtLeast(have, want) {
7524
7925
  }
7525
7926
  async function run2(cmd, args) {
7526
7927
  try {
7527
- const { stdout } = await pexec5(cmd, args, { env: execEnv(), timeout: 1e4, windowsHide: true });
7928
+ const { stdout } = await pexec6(cmd, args, { env: execEnv(), timeout: 1e4, windowsHide: true });
7528
7929
  return String(stdout).trim();
7529
7930
  } catch {
7530
7931
  return null;