@ondrej-svec/hog 1.16.2 → 1.17.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/cli.js CHANGED
@@ -12,7 +12,7 @@ var __export = (target, all) => {
12
12
  // src/config.ts
13
13
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
14
14
  import { homedir } from "os";
15
- import { join } from "path";
15
+ import { isAbsolute, join, normalize } from "path";
16
16
  import { z } from "zod";
17
17
  function migrateConfig(raw) {
18
18
  const version = typeof raw["version"] === "number" ? raw["version"] : 1;
@@ -147,7 +147,7 @@ function requireAuth() {
147
147
  }
148
148
  return auth;
149
149
  }
150
- var CONFIG_DIR, AUTH_FILE, CONFIG_FILE, COMPLETION_ACTION_SCHEMA, REPO_NAME_PATTERN, REPO_CONFIG_SCHEMA, BOARD_CONFIG_SCHEMA, TICKTICK_CONFIG_SCHEMA, PROFILE_SCHEMA, HOG_CONFIG_SCHEMA;
150
+ var CONFIG_DIR, AUTH_FILE, CONFIG_FILE, COMPLETION_ACTION_SCHEMA, REPO_NAME_PATTERN, CLAUDE_START_COMMAND_SCHEMA, REPO_CONFIG_SCHEMA, BOARD_CONFIG_SCHEMA, TICKTICK_CONFIG_SCHEMA, PROFILE_SCHEMA, HOG_CONFIG_SCHEMA;
151
151
  var init_config = __esm({
152
152
  "src/config.ts"() {
153
153
  "use strict";
@@ -160,6 +160,10 @@ var init_config = __esm({
160
160
  z.object({ type: z.literal("addLabel"), label: z.string() })
161
161
  ]);
162
162
  REPO_NAME_PATTERN = /^[\w.-]+\/[\w.-]+$/;
163
+ CLAUDE_START_COMMAND_SCHEMA = z.object({
164
+ command: z.string().min(1),
165
+ extraArgs: z.array(z.string())
166
+ });
163
167
  REPO_CONFIG_SCHEMA = z.object({
164
168
  name: z.string().regex(REPO_NAME_PATTERN, "Must be owner/repo format"),
165
169
  shortName: z.string().min(1),
@@ -167,13 +171,20 @@ var init_config = __esm({
167
171
  statusFieldId: z.string().min(1),
168
172
  dueDateFieldId: z.string().optional(),
169
173
  completionAction: COMPLETION_ACTION_SCHEMA,
170
- statusGroups: z.array(z.string()).optional()
174
+ statusGroups: z.array(z.string()).optional(),
175
+ localPath: z.string().refine((p) => isAbsolute(p), { message: "localPath must be an absolute path" }).refine((p) => normalize(p) === p, {
176
+ message: "localPath must be normalized (no .. segments)"
177
+ }).refine((p) => !p.includes("\0"), { message: "localPath must not contain null bytes" }).optional(),
178
+ claudeStartCommand: CLAUDE_START_COMMAND_SCHEMA.optional()
171
179
  });
172
180
  BOARD_CONFIG_SCHEMA = z.object({
173
181
  refreshInterval: z.number().int().min(10).default(60),
174
182
  backlogLimit: z.number().int().min(1).default(20),
175
183
  assignee: z.string().min(1),
176
- focusDuration: z.number().int().min(60).default(1500)
184
+ focusDuration: z.number().int().min(60).default(1500),
185
+ claudeStartCommand: CLAUDE_START_COMMAND_SCHEMA.optional(),
186
+ claudeLaunchMode: z.enum(["auto", "tmux", "terminal"]).optional(),
187
+ claudeTerminalApp: z.enum(["Terminal", "iTerm", "Ghostty", "WezTerm", "Kitty", "Alacritty"]).optional()
177
188
  });
178
189
  TICKTICK_CONFIG_SCHEMA = z.object({
179
190
  enabled: z.boolean().default(true)
@@ -2070,7 +2081,8 @@ function useKeyboard({
2070
2081
  handleEnterFuzzyPicker,
2071
2082
  handleEnterEditIssue,
2072
2083
  handleUndo,
2073
- handleToggleLog
2084
+ handleToggleLog,
2085
+ handleLaunchClaude
2074
2086
  } = actions;
2075
2087
  const handleInput = useCallback4(
2076
2088
  (input2, key) => {
@@ -2171,6 +2183,10 @@ function useKeyboard({
2171
2183
  }
2172
2184
  return;
2173
2185
  }
2186
+ if (input2 === "C") {
2187
+ handleLaunchClaude();
2188
+ return;
2189
+ }
2174
2190
  }
2175
2191
  if (ui.canAct) {
2176
2192
  const digit = parseInt(input2, 10);
@@ -2309,6 +2325,7 @@ function useKeyboard({
2309
2325
  handleEnterEditIssue,
2310
2326
  handleUndo,
2311
2327
  handleToggleLog,
2328
+ handleLaunchClaude,
2312
2329
  showDetailPanel
2313
2330
  ]
2314
2331
  );
@@ -2834,6 +2851,225 @@ var init_use_ui_state = __esm({
2834
2851
  }
2835
2852
  });
2836
2853
 
2854
+ // src/board/launch-claude.ts
2855
+ var launch_claude_exports = {};
2856
+ __export(launch_claude_exports, {
2857
+ buildPrompt: () => buildPrompt,
2858
+ launchClaude: () => launchClaude
2859
+ });
2860
+ import { spawn, spawnSync } from "child_process";
2861
+ import { existsSync as existsSync5 } from "fs";
2862
+ function buildPrompt(issue) {
2863
+ return `Issue #${issue.number}: ${issue.title}
2864
+ URL: ${issue.url}`;
2865
+ }
2866
+ function isClaudeInPath() {
2867
+ const result = spawnSync("which", ["claude"], { stdio: "pipe" });
2868
+ return result.status === 0;
2869
+ }
2870
+ function isInTmux() {
2871
+ return !!process.env["TMUX"];
2872
+ }
2873
+ function isInSsh() {
2874
+ return !!(process.env["SSH_CLIENT"] ?? process.env["SSH_TTY"]);
2875
+ }
2876
+ function detectTerminalApp() {
2877
+ return process.env["TERM_PROGRAM"];
2878
+ }
2879
+ function resolveCommand(opts) {
2880
+ if (opts.startCommand) return opts.startCommand;
2881
+ return { command: "claude", extraArgs: [] };
2882
+ }
2883
+ function launchViaTmux(opts) {
2884
+ const { localPath, issue, repoFullName } = opts;
2885
+ const { command, extraArgs } = resolveCommand(opts);
2886
+ const prompt = buildPrompt(issue);
2887
+ const windowName = `claude-${issue.number}`;
2888
+ const tmuxArgs = [
2889
+ "new-window",
2890
+ "-d",
2891
+ // don't steal focus from hog board
2892
+ "-c",
2893
+ localPath,
2894
+ "-n",
2895
+ windowName
2896
+ ];
2897
+ if (repoFullName) {
2898
+ tmuxArgs.push("-e", `HOG_REPO=${repoFullName}`);
2899
+ }
2900
+ tmuxArgs.push("-e", `HOG_ISSUE=${issue.number}`);
2901
+ tmuxArgs.push(command, ...extraArgs, "--", prompt);
2902
+ const child = spawn("tmux", tmuxArgs, { stdio: "ignore", detached: true });
2903
+ child.unref();
2904
+ return { ok: true, value: void 0 };
2905
+ }
2906
+ function launchViaTerminalApp(terminalApp, opts) {
2907
+ const { localPath, issue } = opts;
2908
+ const { command, extraArgs } = resolveCommand(opts);
2909
+ const prompt = buildPrompt(issue);
2910
+ const fullCmd = [command, ...extraArgs, "--", prompt].join(" ");
2911
+ switch (terminalApp) {
2912
+ case "iTerm": {
2913
+ const script = `tell application "iTerm"
2914
+ create window with default profile command "bash -c 'cd ${localPath} && ${fullCmd}'"
2915
+ end tell`;
2916
+ const result = spawnSync("osascript", ["-e", script], { stdio: "ignore" });
2917
+ if (result.status !== 0) {
2918
+ return {
2919
+ ok: false,
2920
+ error: {
2921
+ kind: "terminal-failed",
2922
+ message: `iTerm2 launch failed. Is iTerm2 installed and running?`
2923
+ }
2924
+ };
2925
+ }
2926
+ return { ok: true, value: void 0 };
2927
+ }
2928
+ case "Terminal": {
2929
+ const child = spawn("open", ["-a", "Terminal", localPath], {
2930
+ stdio: "ignore",
2931
+ detached: true
2932
+ });
2933
+ child.unref();
2934
+ return { ok: true, value: void 0 };
2935
+ }
2936
+ case "Ghostty": {
2937
+ const child = spawn(
2938
+ "open",
2939
+ ["-na", "Ghostty", "--args", `--working-directory=${localPath}`],
2940
+ { stdio: "ignore", detached: true }
2941
+ );
2942
+ child.unref();
2943
+ return { ok: true, value: void 0 };
2944
+ }
2945
+ case "WezTerm": {
2946
+ const child = spawn("wezterm", ["start", "--cwd", localPath], {
2947
+ stdio: "ignore",
2948
+ detached: true
2949
+ });
2950
+ child.unref();
2951
+ return { ok: true, value: void 0 };
2952
+ }
2953
+ case "Kitty": {
2954
+ const child = spawn(
2955
+ "kitty",
2956
+ ["--directory", localPath, command, ...extraArgs, "--", prompt],
2957
+ {
2958
+ stdio: "ignore",
2959
+ detached: true
2960
+ }
2961
+ );
2962
+ child.unref();
2963
+ return { ok: true, value: void 0 };
2964
+ }
2965
+ case "Alacritty": {
2966
+ const child = spawn(
2967
+ "alacritty",
2968
+ ["--command", "bash", "-c", `cd ${localPath} && ${fullCmd}`],
2969
+ { stdio: "ignore", detached: true }
2970
+ );
2971
+ child.unref();
2972
+ return { ok: true, value: void 0 };
2973
+ }
2974
+ default:
2975
+ return {
2976
+ ok: false,
2977
+ error: {
2978
+ kind: "terminal-app-not-found",
2979
+ message: `Unknown terminal app: ${terminalApp}`
2980
+ }
2981
+ };
2982
+ }
2983
+ }
2984
+ function launchViaDetectedTerminal(opts) {
2985
+ const { terminalApp } = opts;
2986
+ if (terminalApp) {
2987
+ return launchViaTerminalApp(terminalApp, opts);
2988
+ }
2989
+ const termProgram = detectTerminalApp();
2990
+ if (termProgram === "iTerm.app") {
2991
+ return launchViaTerminalApp("iTerm", opts);
2992
+ }
2993
+ if (termProgram === "Apple_Terminal") {
2994
+ return launchViaTerminalApp("Terminal", opts);
2995
+ }
2996
+ if (termProgram === "WezTerm") {
2997
+ return launchViaTerminalApp("WezTerm", opts);
2998
+ }
2999
+ if (termProgram === "ghostty") {
3000
+ return launchViaTerminalApp("Ghostty", opts);
3001
+ }
3002
+ if (process.env["KITTY_WINDOW_ID"]) {
3003
+ return launchViaTerminalApp("Kitty", opts);
3004
+ }
3005
+ if (process.platform === "darwin") {
3006
+ return launchViaTerminalApp("Terminal", opts);
3007
+ }
3008
+ const { localPath, issue } = opts;
3009
+ const { command, extraArgs } = resolveCommand(opts);
3010
+ const prompt = buildPrompt(issue);
3011
+ const child = spawn(
3012
+ "xdg-terminal-exec",
3013
+ ["bash", "-c", `cd ${localPath} && ${[command, ...extraArgs, "--", prompt].join(" ")}`],
3014
+ { stdio: "ignore", detached: true }
3015
+ );
3016
+ child.unref();
3017
+ return { ok: true, value: void 0 };
3018
+ }
3019
+ function launchClaude(opts) {
3020
+ const { localPath, launchMode = "auto" } = opts;
3021
+ if (!existsSync5(localPath)) {
3022
+ return {
3023
+ ok: false,
3024
+ error: {
3025
+ kind: "directory-not-found",
3026
+ message: `Directory not found: ${localPath}. Check localPath config.`
3027
+ }
3028
+ };
3029
+ }
3030
+ if (!isClaudeInPath()) {
3031
+ return {
3032
+ ok: false,
3033
+ error: {
3034
+ kind: "claude-not-found",
3035
+ message: "claude binary not found in PATH. Install Claude Code first."
3036
+ }
3037
+ };
3038
+ }
3039
+ if (isInSsh() && !isInTmux() && launchMode !== "tmux") {
3040
+ return {
3041
+ ok: false,
3042
+ error: {
3043
+ kind: "ssh-no-tmux",
3044
+ message: "Running over SSH without tmux \u2014 start tmux to enable Claude Code launch."
3045
+ }
3046
+ };
3047
+ }
3048
+ const useTmux = launchMode === "tmux" || launchMode === "auto" && isInTmux();
3049
+ if (useTmux) {
3050
+ const result = launchViaTmux(opts);
3051
+ if (!result.ok) {
3052
+ if (launchMode === "tmux") {
3053
+ return {
3054
+ ok: false,
3055
+ error: {
3056
+ kind: "tmux-failed",
3057
+ message: "tmux launch failed. Is tmux running?"
3058
+ }
3059
+ };
3060
+ }
3061
+ return launchViaDetectedTerminal(opts);
3062
+ }
3063
+ return result;
3064
+ }
3065
+ return launchViaDetectedTerminal(opts);
3066
+ }
3067
+ var init_launch_claude = __esm({
3068
+ "src/board/launch-claude.ts"() {
3069
+ "use strict";
3070
+ }
3071
+ });
3072
+
2837
3073
  // src/board/components/action-log.tsx
2838
3074
  import { Box, Text } from "ink";
2839
3075
  import { useEffect as useEffect2, useState as useState6 } from "react";
@@ -3147,7 +3383,7 @@ function HintBar({
3147
3383
  if (uiMode === "overlay:detail") {
3148
3384
  return /* @__PURE__ */ jsxs5(Box5, { children: [
3149
3385
  /* @__PURE__ */ jsx5(Text5, { color: "cyan", bold: true, children: "[DETAIL]" }),
3150
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: " Esc:close e:edit c:comment y:copy-link ? help" })
3386
+ /* @__PURE__ */ jsx5(Text5, { color: "gray", children: " Esc:close e:edit c:comment y:copy-link C:claude ? help" })
3151
3387
  ] });
3152
3388
  }
3153
3389
  if (uiMode.startsWith("overlay:")) {
@@ -3157,7 +3393,7 @@ function HintBar({
3157
3393
  0: "j/k:scroll Esc:close ? help",
3158
3394
  1: "j/k:move Enter:filter 0-4:panel ? help",
3159
3395
  2: "j/k:move Enter:filter Esc:clear 0-4:panel ? help",
3160
- 3: `j/k:move p:pick m:status c:comment /:search n:new 0-4:panel${hasUndoable ? " u:undo" : ""} ? help q:quit`,
3396
+ 3: `j/k:move p:pick m:status c:comment C:claude /:search n:new 0-4:panel${hasUndoable ? " u:undo" : ""} ? help q:quit`,
3161
3397
  4: "j/k:scroll Enter:jump r:refresh 0-4:panel ? help"
3162
3398
  };
3163
3399
  return /* @__PURE__ */ jsxs5(Box5, { children: [
@@ -3258,7 +3494,7 @@ var init_ink_instance = __esm({
3258
3494
  });
3259
3495
 
3260
3496
  // src/board/components/comment-input.tsx
3261
- import { spawnSync } from "child_process";
3497
+ import { spawnSync as spawnSync2 } from "child_process";
3262
3498
  import { mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync4 } from "fs";
3263
3499
  import { tmpdir } from "os";
3264
3500
  import { join as join4 } from "path";
@@ -3312,7 +3548,7 @@ function CommentInput({
3312
3548
  const inkInstance2 = getInkInstance();
3313
3549
  inkInstance2?.clear();
3314
3550
  setRawMode(false);
3315
- spawnSync(cmd, [...extraArgs, tmpFile], { stdio: "inherit" });
3551
+ spawnSync2(cmd, [...extraArgs, tmpFile], { stdio: "inherit" });
3316
3552
  const content = readFileSync4(tmpFile, "utf-8").trim();
3317
3553
  setRawMode(true);
3318
3554
  if (content) {
@@ -3618,7 +3854,7 @@ var init_create_issue_form = __esm({
3618
3854
  });
3619
3855
 
3620
3856
  // src/board/components/edit-issue-overlay.tsx
3621
- import { spawnSync as spawnSync2 } from "child_process";
3857
+ import { spawnSync as spawnSync3 } from "child_process";
3622
3858
  import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
3623
3859
  import { tmpdir as tmpdir2 } from "os";
3624
3860
  import { join as join5 } from "path";
@@ -3743,7 +3979,7 @@ function EditIssueOverlay({
3743
3979
  setRawMode(false);
3744
3980
  while (true) {
3745
3981
  writeFileSync5(tmpFile, currentContent);
3746
- const result = spawnSync2(cmd, [...extraArgs, tmpFile], { stdio: "inherit" });
3982
+ const result = spawnSync3(cmd, [...extraArgs, tmpFile], { stdio: "inherit" });
3747
3983
  if (result.status !== 0 || result.signal !== null || result.error) {
3748
3984
  break;
3749
3985
  }
@@ -4241,7 +4477,8 @@ var init_help_overlay = __esm({
4241
4477
  { key: "y", desc: "Copy issue link to clipboard" },
4242
4478
  { key: "n", desc: "Create new issue" },
4243
4479
  { key: "I", desc: "Natural-language issue create" },
4244
- { key: "l", desc: "Manage labels" }
4480
+ { key: "l", desc: "Manage labels" },
4481
+ { key: "C", desc: "Launch Claude Code session for this issue" }
4245
4482
  ]
4246
4483
  },
4247
4484
  {
@@ -4257,7 +4494,7 @@ var init_help_overlay = __esm({
4257
4494
  });
4258
4495
 
4259
4496
  // src/board/components/nl-create-overlay.tsx
4260
- import { spawnSync as spawnSync3 } from "child_process";
4497
+ import { spawnSync as spawnSync4 } from "child_process";
4261
4498
  import { mkdtempSync as mkdtempSync3, readFileSync as readFileSync6, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
4262
4499
  import { tmpdir as tmpdir3 } from "os";
4263
4500
  import { join as join6 } from "path";
@@ -4342,7 +4579,7 @@ function NlCreateOverlay({
4342
4579
  const inkInstance2 = getInkInstance();
4343
4580
  inkInstance2?.clear();
4344
4581
  setRawMode(false);
4345
- spawnSync3(cmd, [...extraArgs, tmpFile], { stdio: "inherit" });
4582
+ spawnSync4(cmd, [...extraArgs, tmpFile], { stdio: "inherit" });
4346
4583
  const content = readFileSync6(tmpFile, "utf-8");
4347
4584
  setRawMode(true);
4348
4585
  setBody(content.trimEnd());
@@ -5195,7 +5432,7 @@ var init_toast_container = __esm({
5195
5432
  });
5196
5433
 
5197
5434
  // src/board/components/dashboard.tsx
5198
- import { execFileSync as execFileSync3, spawnSync as spawnSync4 } from "child_process";
5435
+ import { execFileSync as execFileSync3, spawnSync as spawnSync5 } from "child_process";
5199
5436
  import { Spinner as Spinner4 } from "@inkjs/ui";
5200
5437
  import { Box as Box24, Text as Text23, useApp, useStdout } from "ink";
5201
5438
  import { useCallback as useCallback12, useEffect as useEffect9, useMemo as useMemo3, useRef as useRef13, useState as useState16 } from "react";
@@ -5724,7 +5961,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
5724
5961
  toast.info(`${label} \u2014 ${found.issue.url}`);
5725
5962
  return;
5726
5963
  }
5727
- const result = spawnSync4(cmd, args, {
5964
+ const result = spawnSync5(cmd, args, {
5728
5965
  input: found.issue.url,
5729
5966
  stdio: ["pipe", "pipe", "pipe"]
5730
5967
  });
@@ -5737,6 +5974,31 @@ function Dashboard({ config: config2, options, activeProfile }) {
5737
5974
  toast.info(`${label} \u2014 ${found.issue.url}`);
5738
5975
  }
5739
5976
  }, [repos, nav.selectedId, config2.repos, toast]);
5977
+ const handleLaunchClaude = useCallback12(() => {
5978
+ const found = findSelectedIssueWithRepo(repos, nav.selectedId);
5979
+ if (!found) return;
5980
+ const rc = config2.repos.find((r) => r.name === found.repoName);
5981
+ if (!rc?.localPath) {
5982
+ toast.info(
5983
+ `Set localPath for ${rc?.shortName ?? found.repoName} in ~/.config/hog/config.json to enable Claude Code launch`
5984
+ );
5985
+ return;
5986
+ }
5987
+ const resolvedStartCommand = rc.claudeStartCommand ?? config2.board.claudeStartCommand;
5988
+ const result = launchClaude({
5989
+ localPath: rc.localPath,
5990
+ issue: { number: found.issue.number, title: found.issue.title, url: found.issue.url },
5991
+ ...resolvedStartCommand ? { startCommand: resolvedStartCommand } : {},
5992
+ launchMode: config2.board.claudeLaunchMode ?? "auto",
5993
+ ...config2.board.claudeTerminalApp ? { terminalApp: config2.board.claudeTerminalApp } : {},
5994
+ repoFullName: found.repoName
5995
+ });
5996
+ if (!result.ok) {
5997
+ toast.error(result.error.message);
5998
+ return;
5999
+ }
6000
+ toast.info(`Claude Code session opened in ${rc.shortName ?? found.repoName}`);
6001
+ }, [repos, nav.selectedId, config2.repos, config2.board, toast]);
5740
6002
  const multiSelectType = useMemo3(() => {
5741
6003
  for (const id of multiSelect.selected) {
5742
6004
  if (id.startsWith("tt:")) return "ticktick";
@@ -5850,7 +6112,8 @@ function Dashboard({ config: config2, options, activeProfile }) {
5850
6112
  handleEnterFuzzyPicker: ui.enterFuzzyPicker,
5851
6113
  handleEnterEditIssue: ui.enterEditIssue,
5852
6114
  handleUndo: undoLast,
5853
- handleToggleLog: () => setLogVisible((v) => !v)
6115
+ handleToggleLog: () => setLogVisible((v) => !v),
6116
+ handleLaunchClaude
5854
6117
  },
5855
6118
  onSearchEscape,
5856
6119
  panelFocus,
@@ -6082,6 +6345,7 @@ var init_dashboard = __esm({
6082
6345
  init_use_panel_focus();
6083
6346
  init_use_toast();
6084
6347
  init_use_ui_state();
6348
+ init_launch_claude();
6085
6349
  init_action_log();
6086
6350
  init_activity_panel();
6087
6351
  init_detail_panel();
@@ -7415,7 +7679,7 @@ function resolveProjectId(projectId) {
7415
7679
  process.exit(1);
7416
7680
  }
7417
7681
  var program = new Command();
7418
- program.name("hog").description("Personal command deck \u2014 unified task dashboard for GitHub Projects + TickTick").version("1.16.2").option("--json", "Force JSON output").option("--human", "Force human-readable output").hook("preAction", (thisCommand) => {
7682
+ program.name("hog").description("Personal command deck \u2014 unified task dashboard for GitHub Projects + TickTick").version("1.17.0").option("--json", "Force JSON output").option("--human", "Force human-readable output").hook("preAction", (thisCommand) => {
7419
7683
  const opts = thisCommand.opts();
7420
7684
  if (opts.json) setFormat("json");
7421
7685
  if (opts.human) setFormat("human");
@@ -7564,6 +7828,63 @@ program.command("pick <issueRef>").description("Pick up an issue: assign to self
7564
7828
  }
7565
7829
  }
7566
7830
  });
7831
+ program.command("launch <issueRef>").description("Launch Claude Code for an issue in its local repo directory").option("--dry-run", "Print resolved config without spawning").action(async (issueRef, opts) => {
7832
+ const cfg = loadFullConfig();
7833
+ const ref = await resolveRef(issueRef, cfg);
7834
+ const rc = ref.repo;
7835
+ if (!rc.localPath) {
7836
+ errorOut(
7837
+ `Set localPath for ${rc.shortName} in ~/.config/hog/config.json to enable Claude Code launch`,
7838
+ { repo: rc.shortName }
7839
+ );
7840
+ }
7841
+ const startCommand = rc.claudeStartCommand ?? cfg.board.claudeStartCommand;
7842
+ const launchMode = cfg.board.claudeLaunchMode ?? "auto";
7843
+ const terminalApp = cfg.board.claudeTerminalApp;
7844
+ if (opts.dryRun) {
7845
+ if (useJson()) {
7846
+ jsonOut({
7847
+ ok: true,
7848
+ dryRun: true,
7849
+ would: {
7850
+ localPath: rc.localPath,
7851
+ command: startCommand?.command ?? "claude",
7852
+ extraArgs: startCommand?.extraArgs ?? [],
7853
+ launchMode,
7854
+ terminalApp: terminalApp ?? null,
7855
+ issueNumber: ref.issueNumber,
7856
+ repo: rc.shortName
7857
+ }
7858
+ });
7859
+ } else {
7860
+ console.log(`[dry-run] Would launch Claude Code for ${rc.shortName}#${ref.issueNumber}`);
7861
+ console.log(` localPath: ${rc.localPath}`);
7862
+ console.log(` command: ${startCommand?.command ?? "claude"}`);
7863
+ console.log(` launchMode: ${launchMode}`);
7864
+ if (terminalApp) console.log(` terminalApp: ${terminalApp}`);
7865
+ }
7866
+ return;
7867
+ }
7868
+ const { launchClaude: launchClaude2 } = await Promise.resolve().then(() => (init_launch_claude(), launch_claude_exports));
7869
+ const { fetchIssueAsync: fetchIssueAsync2 } = await Promise.resolve().then(() => (init_github(), github_exports));
7870
+ const issue = await fetchIssueAsync2(rc.name, ref.issueNumber);
7871
+ const result = launchClaude2({
7872
+ localPath: rc.localPath,
7873
+ issue: { number: issue.number, title: issue.title, url: issue.url },
7874
+ ...startCommand ? { startCommand } : {},
7875
+ launchMode,
7876
+ ...terminalApp ? { terminalApp } : {},
7877
+ repoFullName: rc.name
7878
+ });
7879
+ if (!result.ok) {
7880
+ errorOut(result.error.message, { kind: result.error.kind });
7881
+ }
7882
+ if (useJson()) {
7883
+ jsonOut({ ok: true, data: { repo: rc.shortName, issue: ref.issueNumber } });
7884
+ } else {
7885
+ console.log(`Claude Code session opened in ${rc.shortName}#${ref.issueNumber}`);
7886
+ }
7887
+ });
7567
7888
  var config = program.command("config").description("Manage hog configuration");
7568
7889
  config.command("show").description("Show full configuration").action(() => {
7569
7890
  const cfg = loadFullConfig();