@ondrej-svec/hog 1.16.2 → 1.18.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,14 @@ function useKeyboard({
2171
2183
  }
2172
2184
  return;
2173
2185
  }
2186
+ if (input2 === "C") {
2187
+ handleLaunchClaude();
2188
+ return;
2189
+ }
2190
+ if (input2 === "g") {
2191
+ handleOpen();
2192
+ return;
2193
+ }
2174
2194
  }
2175
2195
  if (ui.canAct) {
2176
2196
  const digit = parseInt(input2, 10);
@@ -2266,7 +2286,11 @@ function useKeyboard({
2266
2286
  onStatusEnter();
2267
2287
  break;
2268
2288
  case 3:
2269
- handleOpen();
2289
+ if (showDetailPanel) {
2290
+ panelFocus.focusPanel(0);
2291
+ } else {
2292
+ ui.enterDetail();
2293
+ }
2270
2294
  break;
2271
2295
  case 4:
2272
2296
  onActivityEnter();
@@ -2309,6 +2333,7 @@ function useKeyboard({
2309
2333
  handleEnterEditIssue,
2310
2334
  handleUndo,
2311
2335
  handleToggleLog,
2336
+ handleLaunchClaude,
2312
2337
  showDetailPanel
2313
2338
  ]
2314
2339
  );
@@ -2834,6 +2859,225 @@ var init_use_ui_state = __esm({
2834
2859
  }
2835
2860
  });
2836
2861
 
2862
+ // src/board/launch-claude.ts
2863
+ var launch_claude_exports = {};
2864
+ __export(launch_claude_exports, {
2865
+ buildPrompt: () => buildPrompt,
2866
+ launchClaude: () => launchClaude
2867
+ });
2868
+ import { spawn, spawnSync } from "child_process";
2869
+ import { existsSync as existsSync5 } from "fs";
2870
+ function buildPrompt(issue) {
2871
+ return `Issue #${issue.number}: ${issue.title}
2872
+ URL: ${issue.url}`;
2873
+ }
2874
+ function isClaudeInPath() {
2875
+ const result = spawnSync("which", ["claude"], { stdio: "pipe" });
2876
+ return result.status === 0;
2877
+ }
2878
+ function isInTmux() {
2879
+ return !!process.env["TMUX"];
2880
+ }
2881
+ function isInSsh() {
2882
+ return !!(process.env["SSH_CLIENT"] ?? process.env["SSH_TTY"]);
2883
+ }
2884
+ function detectTerminalApp() {
2885
+ return process.env["TERM_PROGRAM"];
2886
+ }
2887
+ function resolveCommand(opts) {
2888
+ if (opts.startCommand) return opts.startCommand;
2889
+ return { command: "claude", extraArgs: [] };
2890
+ }
2891
+ function launchViaTmux(opts) {
2892
+ const { localPath, issue, repoFullName } = opts;
2893
+ const { command, extraArgs } = resolveCommand(opts);
2894
+ const prompt = buildPrompt(issue);
2895
+ const windowName = `claude-${issue.number}`;
2896
+ const tmuxArgs = [
2897
+ "new-window",
2898
+ "-d",
2899
+ // don't steal focus from hog board
2900
+ "-c",
2901
+ localPath,
2902
+ "-n",
2903
+ windowName
2904
+ ];
2905
+ if (repoFullName) {
2906
+ tmuxArgs.push("-e", `HOG_REPO=${repoFullName}`);
2907
+ }
2908
+ tmuxArgs.push("-e", `HOG_ISSUE=${issue.number}`);
2909
+ tmuxArgs.push(command, ...extraArgs, "--", prompt);
2910
+ const child = spawn("tmux", tmuxArgs, { stdio: "ignore", detached: true });
2911
+ child.unref();
2912
+ return { ok: true, value: void 0 };
2913
+ }
2914
+ function launchViaTerminalApp(terminalApp, opts) {
2915
+ const { localPath, issue } = opts;
2916
+ const { command, extraArgs } = resolveCommand(opts);
2917
+ const prompt = buildPrompt(issue);
2918
+ const fullCmd = [command, ...extraArgs, "--", prompt].join(" ");
2919
+ switch (terminalApp) {
2920
+ case "iTerm": {
2921
+ const script = `tell application "iTerm"
2922
+ create window with default profile command "bash -c 'cd ${localPath} && ${fullCmd}'"
2923
+ end tell`;
2924
+ const result = spawnSync("osascript", ["-e", script], { stdio: "ignore" });
2925
+ if (result.status !== 0) {
2926
+ return {
2927
+ ok: false,
2928
+ error: {
2929
+ kind: "terminal-failed",
2930
+ message: `iTerm2 launch failed. Is iTerm2 installed and running?`
2931
+ }
2932
+ };
2933
+ }
2934
+ return { ok: true, value: void 0 };
2935
+ }
2936
+ case "Terminal": {
2937
+ const child = spawn("open", ["-a", "Terminal", localPath], {
2938
+ stdio: "ignore",
2939
+ detached: true
2940
+ });
2941
+ child.unref();
2942
+ return { ok: true, value: void 0 };
2943
+ }
2944
+ case "Ghostty": {
2945
+ const child = spawn(
2946
+ "open",
2947
+ ["-na", "Ghostty", "--args", `--working-directory=${localPath}`],
2948
+ { stdio: "ignore", detached: true }
2949
+ );
2950
+ child.unref();
2951
+ return { ok: true, value: void 0 };
2952
+ }
2953
+ case "WezTerm": {
2954
+ const child = spawn("wezterm", ["start", "--cwd", localPath], {
2955
+ stdio: "ignore",
2956
+ detached: true
2957
+ });
2958
+ child.unref();
2959
+ return { ok: true, value: void 0 };
2960
+ }
2961
+ case "Kitty": {
2962
+ const child = spawn(
2963
+ "kitty",
2964
+ ["--directory", localPath, command, ...extraArgs, "--", prompt],
2965
+ {
2966
+ stdio: "ignore",
2967
+ detached: true
2968
+ }
2969
+ );
2970
+ child.unref();
2971
+ return { ok: true, value: void 0 };
2972
+ }
2973
+ case "Alacritty": {
2974
+ const child = spawn(
2975
+ "alacritty",
2976
+ ["--command", "bash", "-c", `cd ${localPath} && ${fullCmd}`],
2977
+ { stdio: "ignore", detached: true }
2978
+ );
2979
+ child.unref();
2980
+ return { ok: true, value: void 0 };
2981
+ }
2982
+ default:
2983
+ return {
2984
+ ok: false,
2985
+ error: {
2986
+ kind: "terminal-app-not-found",
2987
+ message: `Unknown terminal app: ${terminalApp}`
2988
+ }
2989
+ };
2990
+ }
2991
+ }
2992
+ function launchViaDetectedTerminal(opts) {
2993
+ const { terminalApp } = opts;
2994
+ if (terminalApp) {
2995
+ return launchViaTerminalApp(terminalApp, opts);
2996
+ }
2997
+ const termProgram = detectTerminalApp();
2998
+ if (termProgram === "iTerm.app") {
2999
+ return launchViaTerminalApp("iTerm", opts);
3000
+ }
3001
+ if (termProgram === "Apple_Terminal") {
3002
+ return launchViaTerminalApp("Terminal", opts);
3003
+ }
3004
+ if (termProgram === "WezTerm") {
3005
+ return launchViaTerminalApp("WezTerm", opts);
3006
+ }
3007
+ if (termProgram === "ghostty") {
3008
+ return launchViaTerminalApp("Ghostty", opts);
3009
+ }
3010
+ if (process.env["KITTY_WINDOW_ID"]) {
3011
+ return launchViaTerminalApp("Kitty", opts);
3012
+ }
3013
+ if (process.platform === "darwin") {
3014
+ return launchViaTerminalApp("Terminal", opts);
3015
+ }
3016
+ const { localPath, issue } = opts;
3017
+ const { command, extraArgs } = resolveCommand(opts);
3018
+ const prompt = buildPrompt(issue);
3019
+ const child = spawn(
3020
+ "xdg-terminal-exec",
3021
+ ["bash", "-c", `cd ${localPath} && ${[command, ...extraArgs, "--", prompt].join(" ")}`],
3022
+ { stdio: "ignore", detached: true }
3023
+ );
3024
+ child.unref();
3025
+ return { ok: true, value: void 0 };
3026
+ }
3027
+ function launchClaude(opts) {
3028
+ const { localPath, launchMode = "auto" } = opts;
3029
+ if (!existsSync5(localPath)) {
3030
+ return {
3031
+ ok: false,
3032
+ error: {
3033
+ kind: "directory-not-found",
3034
+ message: `Directory not found: ${localPath}. Check localPath config.`
3035
+ }
3036
+ };
3037
+ }
3038
+ if (!isClaudeInPath()) {
3039
+ return {
3040
+ ok: false,
3041
+ error: {
3042
+ kind: "claude-not-found",
3043
+ message: "claude binary not found in PATH. Install Claude Code first."
3044
+ }
3045
+ };
3046
+ }
3047
+ if (isInSsh() && !isInTmux() && launchMode !== "tmux") {
3048
+ return {
3049
+ ok: false,
3050
+ error: {
3051
+ kind: "ssh-no-tmux",
3052
+ message: "Running over SSH without tmux \u2014 start tmux to enable Claude Code launch."
3053
+ }
3054
+ };
3055
+ }
3056
+ const useTmux = launchMode === "tmux" || launchMode === "auto" && isInTmux();
3057
+ if (useTmux) {
3058
+ const result = launchViaTmux(opts);
3059
+ if (!result.ok) {
3060
+ if (launchMode === "tmux") {
3061
+ return {
3062
+ ok: false,
3063
+ error: {
3064
+ kind: "tmux-failed",
3065
+ message: "tmux launch failed. Is tmux running?"
3066
+ }
3067
+ };
3068
+ }
3069
+ return launchViaDetectedTerminal(opts);
3070
+ }
3071
+ return result;
3072
+ }
3073
+ return launchViaDetectedTerminal(opts);
3074
+ }
3075
+ var init_launch_claude = __esm({
3076
+ "src/board/launch-claude.ts"() {
3077
+ "use strict";
3078
+ }
3079
+ });
3080
+
2837
3081
  // src/board/components/action-log.tsx
2838
3082
  import { Box, Text } from "ink";
2839
3083
  import { useEffect as useEffect2, useState as useState6 } from "react";
@@ -3147,7 +3391,7 @@ function HintBar({
3147
3391
  if (uiMode === "overlay:detail") {
3148
3392
  return /* @__PURE__ */ jsxs5(Box5, { children: [
3149
3393
  /* @__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" })
3394
+ /* @__PURE__ */ jsx5(Text5, { color: "gray", children: " Esc:close e:edit c:comment g:open y:copy-link C:claude ? help" })
3151
3395
  ] });
3152
3396
  }
3153
3397
  if (uiMode.startsWith("overlay:")) {
@@ -3157,7 +3401,7 @@ function HintBar({
3157
3401
  0: "j/k:scroll Esc:close ? help",
3158
3402
  1: "j/k:move Enter:filter 0-4:panel ? help",
3159
3403
  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`,
3404
+ 3: `j/k:move Enter:detail g:open p:pick m:status c:comment C:claude /:search n:new 0-4:panel${hasUndoable ? " u:undo" : ""} ? help q:quit`,
3161
3405
  4: "j/k:scroll Enter:jump r:refresh 0-4:panel ? help"
3162
3406
  };
3163
3407
  return /* @__PURE__ */ jsxs5(Box5, { children: [
@@ -3258,7 +3502,7 @@ var init_ink_instance = __esm({
3258
3502
  });
3259
3503
 
3260
3504
  // src/board/components/comment-input.tsx
3261
- import { spawnSync } from "child_process";
3505
+ import { spawnSync as spawnSync2 } from "child_process";
3262
3506
  import { mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync4 } from "fs";
3263
3507
  import { tmpdir } from "os";
3264
3508
  import { join as join4 } from "path";
@@ -3312,7 +3556,7 @@ function CommentInput({
3312
3556
  const inkInstance2 = getInkInstance();
3313
3557
  inkInstance2?.clear();
3314
3558
  setRawMode(false);
3315
- spawnSync(cmd, [...extraArgs, tmpFile], { stdio: "inherit" });
3559
+ spawnSync2(cmd, [...extraArgs, tmpFile], { stdio: "inherit" });
3316
3560
  const content = readFileSync4(tmpFile, "utf-8").trim();
3317
3561
  setRawMode(true);
3318
3562
  if (content) {
@@ -3618,7 +3862,7 @@ var init_create_issue_form = __esm({
3618
3862
  });
3619
3863
 
3620
3864
  // src/board/components/edit-issue-overlay.tsx
3621
- import { spawnSync as spawnSync2 } from "child_process";
3865
+ import { spawnSync as spawnSync3 } from "child_process";
3622
3866
  import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
3623
3867
  import { tmpdir as tmpdir2 } from "os";
3624
3868
  import { join as join5 } from "path";
@@ -3743,7 +3987,7 @@ function EditIssueOverlay({
3743
3987
  setRawMode(false);
3744
3988
  while (true) {
3745
3989
  writeFileSync5(tmpFile, currentContent);
3746
- const result = spawnSync2(cmd, [...extraArgs, tmpFile], { stdio: "inherit" });
3990
+ const result = spawnSync3(cmd, [...extraArgs, tmpFile], { stdio: "inherit" });
3747
3991
  if (result.status !== 0 || result.signal !== null || result.error) {
3748
3992
  break;
3749
3993
  }
@@ -4216,7 +4460,7 @@ var init_help_overlay = __esm({
4216
4460
  {
4217
4461
  category: "View",
4218
4462
  items: [
4219
- { key: "Enter", desc: "Open issue in browser" },
4463
+ { key: "Enter", desc: "Open detail panel" },
4220
4464
  { key: "0", desc: "Detail panel (full-screen on narrow terminals)" },
4221
4465
  { key: "Space", desc: "Multi-select item" },
4222
4466
  { key: "/", desc: "Search (inline filter)" },
@@ -4236,12 +4480,13 @@ var init_help_overlay = __esm({
4236
4480
  { key: "e", desc: "Edit issue in $EDITOR (title, assignee, status, labels)" },
4237
4481
  { key: "c", desc: "Comment on issue" },
4238
4482
  { key: "m", desc: "Move status" },
4239
- { key: "e", desc: "Edit issue in $EDITOR" },
4483
+ { key: "g", desc: "Open issue in browser" },
4240
4484
  { key: "o", desc: "Open Slack thread" },
4241
4485
  { key: "y", desc: "Copy issue link to clipboard" },
4242
4486
  { key: "n", desc: "Create new issue" },
4243
4487
  { key: "I", desc: "Natural-language issue create" },
4244
- { key: "l", desc: "Manage labels" }
4488
+ { key: "l", desc: "Manage labels" },
4489
+ { key: "C", desc: "Launch Claude Code session for this issue" }
4245
4490
  ]
4246
4491
  },
4247
4492
  {
@@ -4257,7 +4502,7 @@ var init_help_overlay = __esm({
4257
4502
  });
4258
4503
 
4259
4504
  // src/board/components/nl-create-overlay.tsx
4260
- import { spawnSync as spawnSync3 } from "child_process";
4505
+ import { spawnSync as spawnSync4 } from "child_process";
4261
4506
  import { mkdtempSync as mkdtempSync3, readFileSync as readFileSync6, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
4262
4507
  import { tmpdir as tmpdir3 } from "os";
4263
4508
  import { join as join6 } from "path";
@@ -4342,7 +4587,7 @@ function NlCreateOverlay({
4342
4587
  const inkInstance2 = getInkInstance();
4343
4588
  inkInstance2?.clear();
4344
4589
  setRawMode(false);
4345
- spawnSync3(cmd, [...extraArgs, tmpFile], { stdio: "inherit" });
4590
+ spawnSync4(cmd, [...extraArgs, tmpFile], { stdio: "inherit" });
4346
4591
  const content = readFileSync6(tmpFile, "utf-8");
4347
4592
  setRawMode(true);
4348
4593
  setBody(content.trimEnd());
@@ -5195,7 +5440,7 @@ var init_toast_container = __esm({
5195
5440
  });
5196
5441
 
5197
5442
  // src/board/components/dashboard.tsx
5198
- import { execFileSync as execFileSync3, spawnSync as spawnSync4 } from "child_process";
5443
+ import { execFileSync as execFileSync3, spawnSync as spawnSync5 } from "child_process";
5199
5444
  import { Spinner as Spinner4 } from "@inkjs/ui";
5200
5445
  import { Box as Box24, Text as Text23, useApp, useStdout } from "ink";
5201
5446
  import { useCallback as useCallback12, useEffect as useEffect9, useMemo as useMemo3, useRef as useRef13, useState as useState16 } from "react";
@@ -5724,7 +5969,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
5724
5969
  toast.info(`${label} \u2014 ${found.issue.url}`);
5725
5970
  return;
5726
5971
  }
5727
- const result = spawnSync4(cmd, args, {
5972
+ const result = spawnSync5(cmd, args, {
5728
5973
  input: found.issue.url,
5729
5974
  stdio: ["pipe", "pipe", "pipe"]
5730
5975
  });
@@ -5737,6 +5982,31 @@ function Dashboard({ config: config2, options, activeProfile }) {
5737
5982
  toast.info(`${label} \u2014 ${found.issue.url}`);
5738
5983
  }
5739
5984
  }, [repos, nav.selectedId, config2.repos, toast]);
5985
+ const handleLaunchClaude = useCallback12(() => {
5986
+ const found = findSelectedIssueWithRepo(repos, nav.selectedId);
5987
+ if (!found) return;
5988
+ const rc = config2.repos.find((r) => r.name === found.repoName);
5989
+ if (!rc?.localPath) {
5990
+ toast.info(
5991
+ `Set localPath for ${rc?.shortName ?? found.repoName} in ~/.config/hog/config.json to enable Claude Code launch`
5992
+ );
5993
+ return;
5994
+ }
5995
+ const resolvedStartCommand = rc.claudeStartCommand ?? config2.board.claudeStartCommand;
5996
+ const result = launchClaude({
5997
+ localPath: rc.localPath,
5998
+ issue: { number: found.issue.number, title: found.issue.title, url: found.issue.url },
5999
+ ...resolvedStartCommand ? { startCommand: resolvedStartCommand } : {},
6000
+ launchMode: config2.board.claudeLaunchMode ?? "auto",
6001
+ ...config2.board.claudeTerminalApp ? { terminalApp: config2.board.claudeTerminalApp } : {},
6002
+ repoFullName: found.repoName
6003
+ });
6004
+ if (!result.ok) {
6005
+ toast.error(result.error.message);
6006
+ return;
6007
+ }
6008
+ toast.info(`Claude Code session opened in ${rc.shortName ?? found.repoName}`);
6009
+ }, [repos, nav.selectedId, config2.repos, config2.board, toast]);
5740
6010
  const multiSelectType = useMemo3(() => {
5741
6011
  for (const id of multiSelect.selected) {
5742
6012
  if (id.startsWith("tt:")) return "ticktick";
@@ -5850,7 +6120,8 @@ function Dashboard({ config: config2, options, activeProfile }) {
5850
6120
  handleEnterFuzzyPicker: ui.enterFuzzyPicker,
5851
6121
  handleEnterEditIssue: ui.enterEditIssue,
5852
6122
  handleUndo: undoLast,
5853
- handleToggleLog: () => setLogVisible((v) => !v)
6123
+ handleToggleLog: () => setLogVisible((v) => !v),
6124
+ handleLaunchClaude
5854
6125
  },
5855
6126
  onSearchEscape,
5856
6127
  panelFocus,
@@ -6082,6 +6353,7 @@ var init_dashboard = __esm({
6082
6353
  init_use_panel_focus();
6083
6354
  init_use_toast();
6084
6355
  init_use_ui_state();
6356
+ init_launch_claude();
6085
6357
  init_action_log();
6086
6358
  init_activity_panel();
6087
6359
  init_detail_panel();
@@ -7415,7 +7687,7 @@ function resolveProjectId(projectId) {
7415
7687
  process.exit(1);
7416
7688
  }
7417
7689
  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) => {
7690
+ program.name("hog").description("Personal command deck \u2014 unified task dashboard for GitHub Projects + TickTick").version("1.18.0").option("--json", "Force JSON output").option("--human", "Force human-readable output").hook("preAction", (thisCommand) => {
7419
7691
  const opts = thisCommand.opts();
7420
7692
  if (opts.json) setFormat("json");
7421
7693
  if (opts.human) setFormat("human");
@@ -7564,6 +7836,63 @@ program.command("pick <issueRef>").description("Pick up an issue: assign to self
7564
7836
  }
7565
7837
  }
7566
7838
  });
7839
+ 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) => {
7840
+ const cfg = loadFullConfig();
7841
+ const ref = await resolveRef(issueRef, cfg);
7842
+ const rc = ref.repo;
7843
+ if (!rc.localPath) {
7844
+ errorOut(
7845
+ `Set localPath for ${rc.shortName} in ~/.config/hog/config.json to enable Claude Code launch`,
7846
+ { repo: rc.shortName }
7847
+ );
7848
+ }
7849
+ const startCommand = rc.claudeStartCommand ?? cfg.board.claudeStartCommand;
7850
+ const launchMode = cfg.board.claudeLaunchMode ?? "auto";
7851
+ const terminalApp = cfg.board.claudeTerminalApp;
7852
+ if (opts.dryRun) {
7853
+ if (useJson()) {
7854
+ jsonOut({
7855
+ ok: true,
7856
+ dryRun: true,
7857
+ would: {
7858
+ localPath: rc.localPath,
7859
+ command: startCommand?.command ?? "claude",
7860
+ extraArgs: startCommand?.extraArgs ?? [],
7861
+ launchMode,
7862
+ terminalApp: terminalApp ?? null,
7863
+ issueNumber: ref.issueNumber,
7864
+ repo: rc.shortName
7865
+ }
7866
+ });
7867
+ } else {
7868
+ console.log(`[dry-run] Would launch Claude Code for ${rc.shortName}#${ref.issueNumber}`);
7869
+ console.log(` localPath: ${rc.localPath}`);
7870
+ console.log(` command: ${startCommand?.command ?? "claude"}`);
7871
+ console.log(` launchMode: ${launchMode}`);
7872
+ if (terminalApp) console.log(` terminalApp: ${terminalApp}`);
7873
+ }
7874
+ return;
7875
+ }
7876
+ const { launchClaude: launchClaude2 } = await Promise.resolve().then(() => (init_launch_claude(), launch_claude_exports));
7877
+ const { fetchIssueAsync: fetchIssueAsync2 } = await Promise.resolve().then(() => (init_github(), github_exports));
7878
+ const issue = await fetchIssueAsync2(rc.name, ref.issueNumber);
7879
+ const result = launchClaude2({
7880
+ localPath: rc.localPath,
7881
+ issue: { number: issue.number, title: issue.title, url: issue.url },
7882
+ ...startCommand ? { startCommand } : {},
7883
+ launchMode,
7884
+ ...terminalApp ? { terminalApp } : {},
7885
+ repoFullName: rc.name
7886
+ });
7887
+ if (!result.ok) {
7888
+ errorOut(result.error.message, { kind: result.error.kind });
7889
+ }
7890
+ if (useJson()) {
7891
+ jsonOut({ ok: true, data: { repo: rc.shortName, issue: ref.issueNumber } });
7892
+ } else {
7893
+ console.log(`Claude Code session opened in ${rc.shortName}#${ref.issueNumber}`);
7894
+ }
7895
+ });
7567
7896
  var config = program.command("config").description("Manage hog configuration");
7568
7897
  config.command("show").description("Show full configuration").action(() => {
7569
7898
  const cfg = loadFullConfig();