@cruxy/cli 0.29.4 → 0.29.5

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.
@@ -1,7 +1,7 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { z } from "zod";
4
- import { resolveToolPath } from "./paths.js";
4
+ import { resolveToolPath, toPosix } from "./paths.js";
5
5
  import { applyEol, detectEol, findMatch, tierLabel } from "./match.js";
6
6
  /** How many leading lines of a created file the approval preview shows. */
7
7
  const PREVIEW_LINES = 20;
@@ -116,7 +116,9 @@ export const applyPatchTool = {
116
116
  };
117
117
  /** Validate one operation against the filesystem and compute its final bytes. */
118
118
  async function planOp(i, op, abs, ctx) {
119
- const rel = path.relative(ctx.cwd, abs);
119
+ // Forward-slash for model-facing output (the `applied` lines and error
120
+ // messages), consistent with every other path tool — see {@link toPosix}.
121
+ const rel = toPosix(path.relative(ctx.cwd, abs));
120
122
  if (op.type === "create") {
121
123
  if (await exists(abs)) {
122
124
  return { ok: false, error: opError(i, op, "file already exists") };
@@ -69,6 +69,16 @@ export declare function resolveInRoot(ctx: ToolContext, p: string): Promise<stri
69
69
  * shared by `glob` and `grep_files` so the two walk-rooted tools cannot diverge
70
70
  * (closing G1/G2). Path *arguments* go through {@link resolveToolPath}; glob
71
71
  * *patterns* are not resolvable paths, so this predicate guards the walk instead.
72
+ *
73
+ * NB — a known internal inconsistency, documented rather than fixed: this split on
74
+ * `/[/\\]/` (and the sibling `firstSegmentIsRoot` in grep_files) treats `\` as a
75
+ * separator on EVERY platform, but the resolution kernel {@link confineToRoot} →
76
+ * `path.resolve` does NOT on POSIX (there `\` is a legal filename character). The
77
+ * two only diverge for a `..\`-style argument on POSIX, which is unreachable from
78
+ * anything the tools EMIT (grep/apply_patch produce `\` only on Windows, where
79
+ * `path.resolve` does treat it as a separator). It is left as-is deliberately:
80
+ * "fixing" confineToRoot to fold `\`→`/` on POSIX would break access to files
81
+ * whose names legitimately contain a backslash.
72
82
  */
73
83
  export declare function isEscapingPattern(pattern: string): boolean;
74
84
  /**
@@ -76,6 +86,25 @@ export declare function isEscapingPattern(pattern: string): boolean;
76
86
  * constant so every no-path tool that fans across roots renders the same label.
77
87
  */
78
88
  export declare const ROOT_LABEL_SEP = " \u25B8 ";
89
+ /**
90
+ * Rewrite the OS path separator to forward slashes for MODEL-FACING output — the
91
+ * one display convention every path tool shares. The rest of the toolset already
92
+ * emits POSIX paths on Windows (the indexer normalises the same way in
93
+ * `indexing/walker.ts`, and git's porcelain and tinyglobby both hand back `/`);
94
+ * this seam is what keeps the two `path.relative`-based stragglers, `grep_files`
95
+ * and `apply_patch`, from drifting to native `src\b.ts`.
96
+ *
97
+ * It splits on `path.sep`, NOT a literal `\`, so it only ever rewrites the real OS
98
+ * separator: on POSIX (`path.sep === "/"`) it is a no-op that PRESERVES a literal
99
+ * backslash in a filename — legal on POSIX — rather than mangling it; on Windows
100
+ * (`path.sep === "\\"`) it turns `path.relative`'s `src\b.ts` into `src/b.ts`.
101
+ *
102
+ * Purely cosmetic at the boundary — the model never needs native separators back:
103
+ * a path it echoes into read_file/edit_file resolves through {@link confineToRoot}
104
+ * → `path.resolve`, which accepts `/` on Windows, so the forward-slash form
105
+ * round-trips as-is.
106
+ */
107
+ export declare function toPosix(p: string): string;
79
108
  /**
80
109
  * Prefix a root-relative path with its root name — but ONLY in a genuine
81
110
  * multi-root session. In single-root the label is dropped so output stays
@@ -92,6 +92,16 @@ export async function resolveInRoot(ctx, p) {
92
92
  * shared by `glob` and `grep_files` so the two walk-rooted tools cannot diverge
93
93
  * (closing G1/G2). Path *arguments* go through {@link resolveToolPath}; glob
94
94
  * *patterns* are not resolvable paths, so this predicate guards the walk instead.
95
+ *
96
+ * NB — a known internal inconsistency, documented rather than fixed: this split on
97
+ * `/[/\\]/` (and the sibling `firstSegmentIsRoot` in grep_files) treats `\` as a
98
+ * separator on EVERY platform, but the resolution kernel {@link confineToRoot} →
99
+ * `path.resolve` does NOT on POSIX (there `\` is a legal filename character). The
100
+ * two only diverge for a `..\`-style argument on POSIX, which is unreachable from
101
+ * anything the tools EMIT (grep/apply_patch produce `\` only on Windows, where
102
+ * `path.resolve` does treat it as a separator). It is left as-is deliberately:
103
+ * "fixing" confineToRoot to fold `\`→`/` on POSIX would break access to files
104
+ * whose names legitimately contain a backslash.
95
105
  */
96
106
  export function isEscapingPattern(pattern) {
97
107
  return path.isAbsolute(pattern) || pattern.split(/[/\\]/).includes("..");
@@ -101,6 +111,27 @@ export function isEscapingPattern(pattern) {
101
111
  * constant so every no-path tool that fans across roots renders the same label.
102
112
  */
103
113
  export const ROOT_LABEL_SEP = " ▸ ";
114
+ /**
115
+ * Rewrite the OS path separator to forward slashes for MODEL-FACING output — the
116
+ * one display convention every path tool shares. The rest of the toolset already
117
+ * emits POSIX paths on Windows (the indexer normalises the same way in
118
+ * `indexing/walker.ts`, and git's porcelain and tinyglobby both hand back `/`);
119
+ * this seam is what keeps the two `path.relative`-based stragglers, `grep_files`
120
+ * and `apply_patch`, from drifting to native `src\b.ts`.
121
+ *
122
+ * It splits on `path.sep`, NOT a literal `\`, so it only ever rewrites the real OS
123
+ * separator: on POSIX (`path.sep === "/"`) it is a no-op that PRESERVES a literal
124
+ * backslash in a filename — legal on POSIX — rather than mangling it; on Windows
125
+ * (`path.sep === "\\"`) it turns `path.relative`'s `src\b.ts` into `src/b.ts`.
126
+ *
127
+ * Purely cosmetic at the boundary — the model never needs native separators back:
128
+ * a path it echoes into read_file/edit_file resolves through {@link confineToRoot}
129
+ * → `path.resolve`, which accepts `/` on Windows, so the forward-slash form
130
+ * round-trips as-is.
131
+ */
132
+ export function toPosix(p) {
133
+ return p.split(path.sep).join("/");
134
+ }
104
135
  /**
105
136
  * Prefix a root-relative path with its root name — but ONLY in a genuine
106
137
  * multi-root session. In single-root the label is dropped so output stays
@@ -109,11 +140,13 @@ export const ROOT_LABEL_SEP = " ▸ ";
109
140
  * point at a different root than the one that was walked (the honesty pin).
110
141
  */
111
142
  export function labelPath(root, rel, isMultiRoot) {
112
- return isMultiRoot ? labelName(root.name, rel) : rel;
143
+ // `rel` is normalised to forward slashes exactly once on each branch: via
144
+ // {@link labelName} when labelled, or directly when the label is dropped.
145
+ return isMultiRoot ? labelName(root.name, rel) : toPosix(rel);
113
146
  }
114
147
  /** As {@link labelPath} but from a bare root name (for hits that carry only a name). */
115
148
  export function labelName(rootName, rel) {
116
- return `${rootName}${ROOT_LABEL_SEP}${rel}`;
149
+ return `${rootName}${ROOT_LABEL_SEP}${toPosix(rel)}`;
117
150
  }
118
151
  /**
119
152
  * The Funnel-B selector for **no-path** tools (`glob`, `grep_files`, `list_files`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cruxy/cli",
3
- "version": "0.29.4",
3
+ "version": "0.29.5",
4
4
  "description": "an agentic coding CLI",
5
5
  "type": "module",
6
6
  "bin": {