@xynogen/pix-hunk 0.1.2 → 0.2.1

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.
Files changed (3) hide show
  1. package/README.md +24 -8
  2. package/package.json +5 -5
  3. package/src/index.ts +35 -30
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # @xynogen/pix-hunk
1
+ # pix-hunk
2
2
 
3
3
  Pi tool for driving a live [Hunk](https://github.com/modem-dev/hunk) diff review.
4
4
  One `hunk` call accepts ordered `ops[]`, so an agent can inspect, navigate,
@@ -7,7 +7,10 @@ quoting.
7
7
 
8
8
  ## Requirements
9
9
 
10
- Install Hunk and launch its interactive TUI in another terminal. This extension
10
+ Launch Hunk's interactive TUI in another terminal. If `hunk` isn't found
11
+ (`~/.pi/agent/binary.json` → `~/.pi/agent/bin` → `PATH`), pix downloads the
12
+ official `modem-dev/hunk` release on first use and verifies it against
13
+ `SHA256SUMS`, with a visible status. This extension
11
14
  only calls `hunk session *`; it never starts `hunk diff`, `show`, `patch`, or
12
15
  another interactive command.
13
16
 
@@ -54,20 +57,33 @@ Model-facing output defaults to 10,000 characters. Set `maxCharacters` between
54
57
  quota, so a large patch cannot hide later results. Full structured results
55
58
  remain available in tool details for inspection.
56
59
 
60
+ ## Scope ceiling
61
+
62
+ `comment apply`, STML markup, and comment clearing are intentionally omitted.
63
+ Use Hunk's CLI directly until repeated demand justifies extending the schema.
64
+
57
65
  ## Install
58
66
 
59
67
  ```bash
60
68
  pi install npm:@xynogen/pix-hunk
61
69
  ```
62
70
 
63
- Standalone/opt-in: Hunk and a live local review session are required, so this
64
- package is not bundled by `@xynogen/pix-core`.
71
+ > Standalone and opt-in. [`@xynogen/pix-core`](https://www.npmjs.com/package/@xynogen/pix-core) does not bundle it. It needs the external Hunk CLI and a live review session.
65
72
 
66
- ## Scope ceiling
73
+ ## Full distro
67
74
 
68
- `comment apply`, STML markup, and comment clearing are intentionally omitted.
69
- Use Hunk's CLI directly until repeated demand justifies extending the schema.
75
+ This package is part of [Pix](https://github.com/xynogen/pix-mono). The installer sets up Pi and the full distro. See [Install](https://github.com/xynogen/pix-mono#install) for the notes for each OS.
76
+
77
+ ```bash
78
+ # Linux / macOS
79
+ curl -fsSL https://raw.githubusercontent.com/xynogen/pix-mono/main/scripts/install.sh | sh
80
+ ```
81
+
82
+ ```powershell
83
+ # Windows
84
+ irm https://raw.githubusercontent.com/xynogen/pix-mono/main/scripts/install.ps1 | iex
85
+ ```
70
86
 
71
87
  ## License
72
88
 
73
- MIT
89
+ MIT. See [LICENSE](LICENSE).
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xynogen/pix-hunk",
3
- "version": "0.1.2",
4
- "description": "Pi tool — drive live Hunk diff reviews with ordered bulk operations",
3
+ "version": "0.2.1",
4
+ "description": "Pi tool \u2014 drive live Hunk diff reviews with ordered bulk operations",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
7
7
  "exports": {
@@ -44,11 +44,11 @@
44
44
  "access": "public"
45
45
  },
46
46
  "dependencies": {
47
- "@xynogen/pix-pretty": "^1.25.0",
48
- "@xynogen/pix-runtime": "^0.8.1",
49
- "typebox": "^1.1.38"
47
+ "@xynogen/pix-pretty": "^1.30.0",
48
+ "@xynogen/pix-runtime": "^0.13.0"
50
49
  },
51
50
  "peerDependencies": {
51
+ "typebox": "*",
52
52
  "@earendil-works/pi-coding-agent": "*",
53
53
  "@earendil-works/pi-tui": "*"
54
54
  }
package/src/index.ts CHANGED
@@ -1,13 +1,16 @@
1
- import { execFile } from "node:child_process";
2
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
2
  import { Text } from "@earendil-works/pi-tui";
3
+ import { reportToolStatus, type ToolStatusUI } from "@xynogen/pix-pretty/tool-status";
4
4
  import {
5
5
  dotJoin,
6
6
  formatCollapsedToolRow,
7
+ formatToolCallTitle,
7
8
  frameToolResult,
9
+ getErrorMessage,
8
10
  hideCollapsedToolCall,
9
11
  } from "@xynogen/pix-pretty/utils";
10
12
  import { type CollapseState, tickCollapse } from "@xynogen/pix-runtime/collapse";
13
+ import { runTool } from "@xynogen/pix-runtime/exec";
11
14
  import { Type } from "typebox";
12
15
  import {
13
16
  buildHunkArgs,
@@ -26,22 +29,29 @@ export interface HunkRunResult {
26
29
  code: number;
27
30
  }
28
31
 
29
- export type HunkRunner = (args: string[], signal?: AbortSignal) => Promise<HunkRunResult>;
30
-
31
- export const runHunk: HunkRunner = (args, signal) =>
32
- new Promise((resolve, reject) => {
33
- execFile("hunk", args, { maxBuffer: MAX_OUTPUT_BYTES, signal }, (error, stdout, stderr) => {
34
- if (error && signal?.aborted) {
35
- reject(error);
36
- return;
37
- }
38
- resolve({
39
- stdout,
40
- stderr: stderr || error?.message || "",
41
- code: typeof error?.code === "number" ? error.code : error ? 1 : 0,
42
- });
43
- });
32
+ export type HunkRunner = (
33
+ args: string[],
34
+ signal?: AbortSignal,
35
+ ui?: ToolStatusUI,
36
+ ) => Promise<HunkRunResult>;
37
+
38
+ /**
39
+ * Default runner: resolve hunk via pix-runtime (binary.json → agent bin →
40
+ * PATH), downloading the official release on first use with a visible status.
41
+ * runTool also runs the npm `hunk.cmd` shim on Windows. A missing binary
42
+ * throws BinaryMissingError carrying the install hint.
43
+ */
44
+ export const runHunk: HunkRunner = async (args, signal, ui) => {
45
+ const r = await runTool("hunk", args, {
46
+ signal,
47
+ onStatus: reportToolStatus(ui),
48
+ maxBuffer: MAX_OUTPUT_BYTES,
44
49
  });
50
+ if (signal?.aborted) throw signal.reason ?? new Error("aborted");
51
+ if (r.truncated)
52
+ return { stdout: r.stdout, stderr: `hunk output exceeded ${MAX_OUTPUT_BYTES} bytes`, code: 1 };
53
+ return { stdout: r.stdout, stderr: r.stderr, code: r.code ?? 1 };
54
+ };
45
55
 
46
56
  type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
47
57
 
@@ -361,10 +371,7 @@ function modelText(results: HunkOperationResult[], ops: ToolOp[], maxCharacters:
361
371
  }
362
372
 
363
373
  function executionError(run: HunkRunResult): string {
364
- const raw = (run.stderr || run.stdout || `hunk exited ${run.code}`).trim();
365
- return /(?:^|\s)(?:spawn\s+)?hunk\s+ENOENT(?:\s|$)/i.test(raw)
366
- ? "Hunk CLI not found. Install Hunk, launch a review session, then retry."
367
- : raw;
374
+ return (run.stderr || run.stdout || `hunk exited ${run.code}`).trim();
368
375
  }
369
376
 
370
377
  function actionSummary(actions: HunkAction[]): string {
@@ -440,6 +447,7 @@ function renderExpanded(details: HunkResultDetails, theme: HunkTheme): string {
440
447
  export default function registerHunk(pi: ExtensionAPI, runner: HunkRunner = runHunk): void {
441
448
  pi.registerTool({
442
449
  name: "hunk",
450
+ ...({ exposure: "deferred" } as const),
443
451
  label: "Hunk",
444
452
  renderShell: "self",
445
453
  description:
@@ -472,7 +480,7 @@ export default function registerHunk(pi: ExtensionAPI, runner: HunkRunner = runH
472
480
  return text;
473
481
  const actions = ((args as { ops?: ToolOp[] }).ops ?? []).map((op) => op.action);
474
482
  text.setText(
475
- `${theme.fg("toolTitle", theme.bold("hunk"))} ${theme.fg("dim", actionSummary(actions))}`,
483
+ `${formatToolCallTitle(theme, "hunk", context)} ${theme.fg("dim", actionSummary(actions))}`,
476
484
  );
477
485
  return text;
478
486
  },
@@ -483,14 +491,11 @@ export default function registerHunk(pi: ExtensionAPI, runner: HunkRunner = runH
483
491
  text.setText(
484
492
  result.content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n"),
485
493
  );
486
- return text;
494
+ return options.isPartial ? text : frameToolResult(text, theme, context.isError);
487
495
  }
488
- const collapsed = tickCollapse(
489
- "hunk",
490
- context.state as CollapseState,
491
- context.invalidate,
492
- options.expanded,
493
- );
496
+ const collapsed =
497
+ !options.isPartial &&
498
+ tickCollapse("hunk", context.state as CollapseState, context.invalidate, options.expanded);
494
499
  if (collapsed) {
495
500
  text.setText(renderSummary(details, theme as HunkTheme));
496
501
  return text;
@@ -505,7 +510,7 @@ export default function registerHunk(pi: ExtensionAPI, runner: HunkRunner = runH
505
510
  const results: HunkOperationResult[] = [];
506
511
  for (const op of ops) {
507
512
  try {
508
- const run = await runner(buildHunkArgs(op, ctx.cwd), signal);
513
+ const run = await runner(buildHunkArgs(op, ctx.cwd), signal, ctx.ui);
509
514
  results.push(
510
515
  run.code === 0
511
516
  ? { action: op.action, ok: true, data: parseOutput(run.stdout || run.stderr) }
@@ -516,7 +521,7 @@ export default function registerHunk(pi: ExtensionAPI, runner: HunkRunner = runH
516
521
  results.push({
517
522
  action: op.action,
518
523
  ok: false,
519
- error: error instanceof Error ? error.message : String(error),
524
+ error: getErrorMessage(error),
520
525
  });
521
526
  }
522
527
  }