phewsh 0.15.56 → 0.15.58

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/bin/phewsh.js CHANGED
@@ -14,6 +14,12 @@ if (command === 'shim-preflight') {
14
14
  process.exit(0);
15
15
  }
16
16
 
17
+ // Smart word-wrap for all prose the CLI prints — wraps at word boundaries
18
+ // instead of the terminal's mid-word hard wrap. TTY-only, idempotent, and it
19
+ // leaves process.stdout.write paths (spinner, streamed AI) alone. Installed
20
+ // after the shim-preflight fast path so that stays instant and untouched.
21
+ try { require('../lib/ui').installSmartWrap(); } catch { /* never block the CLI on cosmetics */ }
22
+
17
23
  // ── ANSI helpers (no chalk dependency)
18
24
  const b = (s) => `\x1b[1m${s}\x1b[0m`; // bold
19
25
  const d = (s) => `\x1b[2m${s}\x1b[0m`; // dim
package/lib/md.js CHANGED
@@ -44,7 +44,7 @@ const ANSI = /\x1b\[[0-9;]*m/g;
44
44
  const vis = (s) => s.replace(ANSI, '').length;
45
45
 
46
46
  function softWrap(s, base = '', hang) {
47
- const width = process.stdout.columns;
47
+ const width = require('./ui').termWidth();
48
48
  if (!width || width < 24 || vis(s) <= width) return s;
49
49
  const lead = s.match(/^ */)[0].length;
50
50
  const indent = hang == null ? lead : hang;
package/lib/ui.js CHANGED
@@ -111,6 +111,86 @@ async function brandReveal(fast = false) {
111
111
  console.log('');
112
112
  }
113
113
 
114
+ // ── Smart line wrap ──────────────────────────────────────
115
+ // The terminal's default hard wrap breaks mid-word at the window edge. This
116
+ // wraps at word boundaries instead (CSS `text-wrap: pretty`, in spirit),
117
+ // ANSI-aware: colour codes carry zero width, and the colour active at a break
118
+ // is re-opened on the continuation line so nothing loses its tint. A word
119
+ // longer than the line is left whole — we never split inside a word, which is
120
+ // the whole point. Per physical line; only over-long lines are touched.
121
+ const SGR = /\x1b\[[0-9;]*m/g;
122
+ const visW = (s) => s.replace(SGR, '').length;
123
+
124
+ // Best available terminal width, with one column of safety margin so content
125
+ // never sits flush against the edge (where terminals add their own wrap).
126
+ // `PHEWSH_WIDTH` pins it explicitly — the escape hatch for host terminals that
127
+ // misreport their size (some embedded/agent terminals advertise a wider pty
128
+ // than the visible pane, which makes text break mid-word even with smart wrap
129
+ // on). Falls back to COLUMNS, then the live tty size, then 80.
130
+ function termWidth() {
131
+ const pin = parseInt(process.env.PHEWSH_WIDTH, 10);
132
+ const base = (Number.isFinite(pin) && pin >= 20)
133
+ ? pin
134
+ : (process.stdout.columns || parseInt(process.env.COLUMNS, 10) || 80);
135
+ return Math.max(24, base - 1);
136
+ }
137
+
138
+ // Active SGR state after consuming `s`, starting from `prev`. A reset clears it;
139
+ // any other colour code accumulates. Good enough to re-open colour at a break.
140
+ function sgrAfter(prev, s) {
141
+ let active = prev;
142
+ for (const code of s.match(SGR) || []) {
143
+ active = (code === '\x1b[0m' || code === '\x1b[m') ? '' : active + code;
144
+ }
145
+ return active;
146
+ }
147
+
148
+ function wrapAnsi(s, width) {
149
+ if (!width || width < 24) return s;
150
+ return s.split('\n').map((line) => {
151
+ if (visW(line) <= width) return line;
152
+ const lead = line.match(/^ */)[0].length;
153
+ const pad = ' '.repeat(lead);
154
+ const words = line.slice(lead).split(/ +/);
155
+ const out = [];
156
+ let cur = pad, len = lead, started = false, active = '';
157
+ for (const word of words) {
158
+ const wl = visW(word);
159
+ if (started && len + 1 + wl > width) {
160
+ out.push(cur + '\x1b[0m'); // close colour cleanly at the break
161
+ cur = active + pad + word; // re-open it on the continuation line
162
+ len = lead + wl;
163
+ } else {
164
+ cur += (started ? ' ' : '') + word;
165
+ len += (started ? 1 : 0) + wl;
166
+ started = true;
167
+ }
168
+ active = sgrAfter(active, word);
169
+ }
170
+ out.push(cur);
171
+ return out.join('\n');
172
+ }).join('\n');
173
+ }
174
+
175
+ // Monkeypatch console.log so every prose line the CLI prints wraps at word
176
+ // boundaries. TTY-only (piped/redirected output stays raw for tooling), and
177
+ // only the simple all-string case is transformed — object args pass through
178
+ // untouched. Idempotent. process.stdout.write paths (spinner, typewriter,
179
+ // streamed AI render) are intentionally left alone; they handle their own.
180
+ function installSmartWrap() {
181
+ if (!process.stdout.isTTY || console.log.__phewshWrapped) return;
182
+ const orig = console.log.bind(console);
183
+ const wrapped = (...args) => {
184
+ if (args.length && args.every((a) => typeof a === 'string')) {
185
+ orig(wrapAnsi(args.join(' '), termWidth()));
186
+ } else {
187
+ orig(...args);
188
+ }
189
+ };
190
+ wrapped.__phewshWrapped = true;
191
+ console.log = wrapped;
192
+ }
193
+
114
194
  // ── Status panel ─────────────────────────────────────────
115
195
  function statusPanel(title, rows) {
116
196
  const maxLabel = Math.max(...rows.map(r => r[0].length));
@@ -267,6 +347,8 @@ module.exports = {
267
347
  // Components
268
348
  spinner, brandReveal, statusPanel, interopLine, divider, typewrite,
269
349
  randomTip, TOUR_PAGES,
350
+ // Smart wrap
351
+ wrapAnsi, installSmartWrap, termWidth,
270
352
  // ANSI helpers
271
353
  hide, show, up, clearLine, sleep,
272
354
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.15.56",
3
+ "version": "0.15.58",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"