omakit 0.4.0 → 0.4.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.
package/README.md CHANGED
@@ -40,6 +40,10 @@ and shows the upgrade command. It installs nothing automatically; scripts
40
40
  and JSON stay quiet. `DISABLE_UPDATE_NOTIFIER=1` disables the notice, and
41
41
  `omakit doctor` checks explicitly. [Update behaviour](docs/INSTALL.md#updating).
42
42
 
43
+ Terminal help and reports adapt to the available width, up to 120 columns
44
+ for readability. Narrow windows wrap sooner; pipes and text files keep the
45
+ stable eighty-column layout. JSON and exact issue bodies remain intact.
46
+
43
47
  ### `submit`
44
48
 
45
49
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omakit",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "The safe place to find out: everything knowable about an Omarchy Quattro plugin submission before you post it, on your own machine. Agent-first, read-only against the marketplace, posts nothing, zero dependencies.",
5
5
  "license": "MIT",
6
6
  "author": "Maarten Tolhuijs",
@@ -81,7 +81,8 @@ side rather than the findings themselves. The GitHub side uses whatever
81
81
  credential `github.mjs` resolves (a `gh` login, and only that), read-only.
82
82
 
83
83
  Two rules about the output, stated as rules because each is an exception to
84
- a wider one. Everything omakit writes itself stays within 80 columns; text
84
+ a wider one. Pipes use 80 columns; terminals use their available width up to
85
+ 120 columns. Text files compose at 80 columns without colour escapes. Text
85
86
  that will be posted verbatim, the marketplace's own baseline report and the
86
87
  issue body rendered from the pinned form, is never wrapped and may exceed 80,
87
88
  because a wrapped body would not be the body. And stdout is the whole result;
@@ -15,7 +15,7 @@
15
15
  // the report ends with the command line that repeats the run without asking.
16
16
 
17
17
  import { createInterface } from "node:readline"
18
- import { colourEnabled, STEP, action, styler, wrap } from "./style.mjs"
18
+ import { colourEnabled, outputColumns, STEP, action, styler, wrap } from "./style.mjs"
19
19
  import { resolveCategory, resolveTags } from "./form.mjs"
20
20
  import { watchIssueTitle } from "./watch.mjs"
21
21
 
@@ -52,10 +52,10 @@ function reader(input) {
52
52
  /** Ask one question until an answer resolves; `parse` returns { ok, value } or { ok: false, reason }. */
53
53
  async function question(lines, output, c, { name, heading, options, defaultIndexes, parse, wrapOptions = false, eofRemedy }) {
54
54
  const step = " ".repeat(STEP)
55
- output.write(`${wrap(heading, {}, c).join("\n")}\n`)
55
+ output.write(`${wrap(heading, { width: outputColumns(output) }, c).join("\n")}\n`)
56
56
  for (const [index, option] of options.entries()) {
57
57
  output.write(wrapOptions
58
- ? `${wrap(`${String(index + 1).padStart(2)} ${option}`, { indent: STEP }, c).join("\n")}\n`
58
+ ? `${wrap(`${String(index + 1).padStart(2)} ${option}`, { indent: STEP, width: outputColumns(output) }, c).join("\n")}\n`
59
59
  : `${step}${c("typeable", String(index + 1).padStart(2))} ${option}\n`)
60
60
  }
61
61
  const fallback = defaultIndexes.length ? defaultIndexes.map((index) => index + 1).join(",") : null
@@ -70,7 +70,7 @@ async function question(lines, output, c, { name, heading, options, defaultIndex
70
70
  const text = raw.trim() || (fallback ?? "")
71
71
  const parsed = parse(text)
72
72
  if (parsed.ok) return parsed.value
73
- output.write(`${wrap(parsed.reason, {}, c).join("\n")}\n`)
73
+ output.write(`${wrap(parsed.reason, { width: outputColumns(output) }, c).join("\n")}\n`)
74
74
  }
75
75
  }
76
76
 
@@ -49,7 +49,7 @@
49
49
  // letters come from the small font below, so renaming the tool is a change to
50
50
  // one string and not a redrawing job.
51
51
 
52
- import { code, colourEnabled, DENSITY, motionEnabled, MOTION, rule as floorRule } from "./style.mjs"
52
+ import { code, colourEnabled, DENSITY, motionEnabled, MOTION, outputColumns, rule as floorRule, wrap } from "./style.mjs"
53
53
  import { effectAvailable, playEffect } from "./effect.mjs"
54
54
 
55
55
  const ESC = "\u001b["
@@ -227,6 +227,15 @@ export async function banner(options = {}) {
227
227
  // and a piped run should differ from a watched one only in decoration.
228
228
  if (!enabled) return
229
229
 
230
+ // Never animate artwork across physical terminal rows: cursor-up would
231
+ // redraw the wrong row. A narrow terminal gets a compact wordmark instead.
232
+ if (stream.isTTY && Number.isFinite(stream.columns) && stream.columns > 0 && stream.columns < width) {
233
+ stream.write(`${c(code("name"), word.toUpperCase())}\n`)
234
+ if (options.tagline) stream.write(`${wrap(options.tagline, { width: outputColumns(stream) }, (name, text) => c(code(name), text)).join("\n")}\n`)
235
+ stream.write("\n")
236
+ return
237
+ }
238
+
230
239
  // A five-row animation redrawn with cursor-up needs five rows that stay put.
231
240
  // In a terminal with no room the screen scrolls under the animation, the
232
241
  // cursor-up lands a line off, and a row from an earlier frame is left stranded
@@ -34,7 +34,7 @@ import { updateCheckEnabled, updateNotice } from "./update-check.mjs"
34
34
  import { progress } from "./progress.mjs"
35
35
  import { banner, bannerEnabled } from "./banner.mjs"
36
36
  import { COMMANDS, renderSummary, renderUsage, TAGLINE } from "./usage.mjs"
37
- import { action, AUDIT_VERDICTS, colourEnabled, GUTTER, labelled, mark, styler, verdict, wrap } from "./style.mjs"
37
+ import { action, AUDIT_VERDICTS, colourEnabled, GUTTER, labelled, mark, outputColumns, styler, verdict, withOutputStream, wrap } from "./style.mjs"
38
38
  import { omakitCacheDir, withHomeAbbreviated } from "./paths.mjs"
39
39
  import { DEFAULTS as WEIGH_DEFAULTS, measureWeigh, planWeigh } from "../weigh/audit.mjs"
40
40
  import { confirmationQuestion, renderList, renderWeigh, renderPlan } from "../weigh/report.mjs"
@@ -76,8 +76,10 @@ const REMEDY = Object.freeze({
76
76
  */
77
77
  function fail(code, message, exit = 1, remedy = REMEDY[code], body = () => []) {
78
78
  const c = styler(colourEnabled(process.stderr))
79
- const lines = [`${mark("fail", c)}${c("name", code)}`, ...wrap(message, { indent: GUTTER }, c), ...body(c)]
80
- if (remedy) lines.push(...action(remedy, c))
79
+ const lines = withOutputStream(process.stderr, () => [
80
+ `${mark("fail", c)}${c("name", code)}`, ...wrap(message, { indent: GUTTER }, c), ...body(c),
81
+ ...(remedy ? action(remedy, c) : []),
82
+ ])
81
83
  process.stderr.write(`${lines.join("\n")}\n`)
82
84
  process.exit(exit)
83
85
  }
@@ -123,6 +125,12 @@ function emit(args, text) {
123
125
  }
124
126
  }
125
127
 
128
+ function reportText(args, render, result, options = {}) {
129
+ const out = option(args, "--out")
130
+ return withOutputStream(out ? { isTTY: false } : process.stdout,
131
+ () => render(result, { ...options, ...(out ? { colour: false } : {}) }))
132
+ }
133
+
126
134
  async function cmdSubmit(args) {
127
135
  const target = positionals(args)[0]
128
136
  if (!target) fail("usage", "submit needs a target: `omakit submit <target> --category <c> --tags <a,b>`", 2)
@@ -174,7 +182,7 @@ async function cmdSubmit(args) {
174
182
  failFrom(error)
175
183
  }
176
184
  spinner.done()
177
- emit(args, args.includes("--json") ? `${JSON.stringify(result, null, 2)}\n` : renderSubmit(result))
185
+ emit(args, args.includes("--json") ? `${JSON.stringify(result, null, 2)}\n` : reportText(args, renderSubmit, result))
178
186
  // Three outcomes, two exit codes: `ready` and `listed` are both healthy
179
187
  // states, and only a refusal is a 1.
180
188
  process.exit(result.outcome === "refused" ? 1 : 0)
@@ -215,7 +223,7 @@ async function cmdWatch(args) {
215
223
  }
216
224
  spinner.done()
217
225
  const render = result.mode === "list" ? renderWatchList : result.mode === "all" ? renderWatchAll : renderWatch
218
- emit(args, args.includes("--json") ? `${JSON.stringify(result, null, 2)}\n` : render(result))
226
+ emit(args, args.includes("--json") ? `${JSON.stringify(result, null, 2)}\n` : reportText(args, render, result))
219
227
  process.exit(result.verdict?.state === "unknown" || result.summary?.unknown > 0 ? 2 : 0)
220
228
  }
221
229
 
@@ -250,7 +258,7 @@ async function cmdDoctor(args) {
250
258
  const spinner = spinnerFor(args)
251
259
  const result = await doctor({ repoRoot: ROOT, offline: args.includes("--offline"), onPhase: spinner.phase })
252
260
  spinner.done()
253
- emit(args, args.includes("--json") ? `${JSON.stringify(result, null, 2)}\n` : renderDoctor(result))
261
+ emit(args, args.includes("--json") ? `${JSON.stringify(result, null, 2)}\n` : reportText(args, renderDoctor, result))
254
262
  process.exit(result.problems ? 1 : 0)
255
263
  }
256
264
 
@@ -293,7 +301,7 @@ async function cmdVerify(args) {
293
301
  const blockingRules = section.invoked && section.official && !section.official.error
294
302
  ? (await consequence(requirePin(ROOT).dir, section.official)).selectivelyBlockingRules
295
303
  : []
296
- emit(args, renderVerify(document, { blockingRules }))
304
+ emit(args, reportText(args, renderVerify, document, { blockingRules }))
297
305
  }
298
306
 
299
307
  async function cmdParity(args) {
@@ -420,7 +428,7 @@ async function cmdWeigh(args) {
420
428
  // The confirmation. The plan is printed either way, so the record says
421
429
  // what was agreed to; the question is asked only at a terminal on both
422
430
  // ends, and --yes is the only other way past it.
423
- narrate.write(`${renderPlan(plan, { colour: colourEnabled(narrate) }).join("\n")}\n`)
431
+ narrate.write(`${withOutputStream(narrate, () => renderPlan(plan, { colour: colourEnabled(narrate) })).join("\n")}\n`)
424
432
  if (!parsed.options.has("--yes")) {
425
433
  const interactive = !json && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY)
426
434
  if (!interactive) notWeighed("not-confirmed", `this restarts the shell ${plan.restarts} times and edits shell.json for the duration; a pipe, an agent or --json cannot answer for the person whose shell it is.`, REMEDY["not-confirmed"], 2)
@@ -487,12 +495,15 @@ const [command, ...rest] = process.argv.slice(2)
487
495
  // stays empty on success, and never for setup, which is the fix.
488
496
  if (updateCheckEnabled({ command, args: rest, stdinTTY: process.stdin.isTTY, stdoutTTY: process.stdout.isTTY, stderrTTY: process.stderr.isTTY })) {
489
497
  const notice = await updateNotice({ repoRoot: ROOT, version: VERSION })
490
- if (notice) process.stderr.write(`${wrap(notice, {}, styler(colourEnabled(process.stderr))).join("\n")}\n`)
498
+ if (notice) process.stderr.write(`${wrap(notice, { width: outputColumns(process.stderr) }, styler(colourEnabled(process.stderr))).join("\n")}\n`)
491
499
  }
492
500
 
493
501
  if (command !== "setup" && process.stderr.isTTY) {
494
502
  const notice = staleCompletionNotice({ version: VERSION })
495
- if (notice) process.stderr.write(`${styler(colourEnabled(process.stderr))("label", notice)}\n`)
503
+ if (notice) {
504
+ const c = styler(colourEnabled(process.stderr))
505
+ process.stderr.write(`${wrap(notice, { width: outputColumns(process.stderr) }).map((line) => c("label", line)).join("\n")}\n`)
506
+ }
496
507
  }
497
508
 
498
509
  if (command === "setup") {
@@ -556,12 +567,12 @@ if (command === "setup") {
556
567
  // The short list on a typo, not 53 lines of reference, in the same register
557
568
  // as every other failure: what happened, what it means, what to run.
558
569
  const c = styler(colourEnabled(process.stderr))
559
- process.stderr.write([
570
+ process.stderr.write(withOutputStream(process.stderr, () => [
560
571
  `${mark("fail", c)}${c("name", "unknown command")}`,
561
572
  ...wrap(`\`${command}\` is not something omakit does. The commands it has are listed below.`, { indent: GUTTER }, c),
562
573
  ...action("omakit help", c),
563
574
  "",
564
- renderSummary({ colour: colourEnabled(process.stderr), heading: false }),
565
- ].join("\n"))
575
+ renderSummary({ stream: process.stderr, colour: colourEnabled(process.stderr), heading: false }),
576
+ ].join("\n")))
566
577
  process.exit(2)
567
578
  }
@@ -18,7 +18,7 @@
18
18
  // screen, and the last screen is the one a person is looking at.
19
19
 
20
20
  import {
21
- action, colourEnabled, COLUMNS, continuation, field, GUTTER, labelled, mark, section, STEP, styler, verdict, width, wrap,
21
+ action, colourEnabled, outputColumns, continuation, field, GUTTER, labelled, mark, section, STEP, styler, verdict, width, wrap,
22
22
  } from "./style.mjs"
23
23
  import { withHomeAbbreviated } from "./paths.mjs"
24
24
  import { watchIssueTitle } from "./watch.mjs"
@@ -42,7 +42,11 @@ function stateOf(check) {
42
42
  export function head(state, id, source, c) {
43
43
  const left = `${mark(state, c)}${c("name", id)}`
44
44
  const tag = c("punctuation", `[${source}]`)
45
- const gap = Math.max(2, COLUMNS - width(left) - width(tag))
45
+ const columns = outputColumns()
46
+ if (width(left) + width(tag) + 2 > columns) {
47
+ return `${left}\n${wrap(`[${source}]`, { indent: GUTTER }).map((line) => c("punctuation", line)).join("\n")}`
48
+ }
49
+ const gap = columns - width(left) - width(tag)
46
50
  return `${left}${" ".repeat(gap)}${tag}`
47
51
  }
48
52
 
@@ -78,7 +82,7 @@ function commandLines(command, c) {
78
82
  units.push(word)
79
83
  }
80
84
  }
81
- const room = COLUMNS - STEP - " \\".length
85
+ const room = outputColumns() - STEP - " \\".length
82
86
  const lines = []
83
87
  let line = ""
84
88
  for (const unit of units) {
@@ -115,9 +115,8 @@ export function code(role) {
115
115
  // --- geometry ---------------------------------------------------------------
116
116
 
117
117
  /**
118
- * The width everything is composed for. Eighty columns is the contract: a
119
- * check's why-paragraph, a remedy, a refusal and the help all wrap inside it,
120
- * and tests/unit/style.test.mjs renders every report and measures. The one
118
+ * The stable width for pipes and files. Terminals use their current column
119
+ * count, up to MAX_COLUMNS so wide windows still have readable prose. The one
121
120
  * thing exempt is the marketplace's own baseline report, which is printed
122
121
  * verbatim because rewrapping somebody else's attestation would be editing it.
123
122
  *
@@ -125,6 +124,23 @@ export function code(role) {
125
124
  * `overflows` below is the measurement, and it is the one both test files use.
126
125
  */
127
126
  export const COLUMNS = 80
127
+ export const MAX_COLUMNS = 120
128
+
129
+ let compositionStream = null
130
+
131
+ /** Resolve at rendering time, so a resized terminal's next output uses its new width. */
132
+ export function outputColumns(stream = compositionStream || process.stdout) {
133
+ return stream?.isTTY && Number.isFinite(stream.columns) && stream.columns >= 1
134
+ ? Math.min(MAX_COLUMNS, Math.floor(stream.columns))
135
+ : COLUMNS
136
+ }
137
+
138
+ /** Scope synchronous composition to its destination, including files and stderr. */
139
+ export function withOutputStream(stream, render) {
140
+ const previous = compositionStream
141
+ compositionStream = stream
142
+ try { return render() } finally { compositionStream = previous }
143
+ }
128
144
 
129
145
  /**
130
146
  * The indent scale, in columns. Three stops, and every line in the tool starts
@@ -327,7 +343,7 @@ export function overflows(line, total = COLUMNS) {
327
343
  // --- composition ------------------------------------------------------------
328
344
 
329
345
  /**
330
- * Wrap prose to the contract width, and paint it.
346
+ * Wrap prose to the destination width, and paint it.
331
347
  *
332
348
  * `indent` is the column the text starts in and is part of the width, which is
333
349
  * the bug the earlier version had: it wrapped at 78 and then indented by 7, and
@@ -346,7 +362,7 @@ export function overflows(line, total = COLUMNS) {
346
362
  * @param {(name: string, text: string) => string} [c]
347
363
  * @returns {string[]} lines, indented and painted
348
364
  */
349
- export function wrap(text, { indent = 0, width: total = COLUMNS, first = indent } = {}, c = styler(false)) {
365
+ export function wrap(text, { indent = 0, width: total = outputColumns(), first = indent } = {}, c = styler(false)) {
350
366
  // A word is a run of non-spaces, or a backticked span with whatever
351
367
  // punctuation clings to it: "(`omakit pin`)." is one word.
352
368
  const words = String(text).match(/[^\s`]*`[^`]*`[^\s`]*|\S+/g) || []
@@ -422,7 +438,7 @@ export function continuation(value, c) {
422
438
  * under itself. There is exactly one of these under any failure, and it is the
423
439
  * only line in the tool that starts with an arrow, so it can be found by shape.
424
440
  */
425
- export function action(text, c, { indent = GUTTER, width: total = COLUMNS } = {}) {
441
+ export function action(text, c, { indent = GUTTER, width: total = outputColumns() } = {}) {
426
442
  // Painted after wrapping, and all of it cyan: the whole line is the thing
427
443
  // to do, so a backticked word inside it has nothing to stand out from.
428
444
  const lines = wrap(text, { indent: indent + 2, width: total })
@@ -449,11 +465,11 @@ export function labelled(label, text, c, { indent = GUTTER } = {}) {
449
465
  * grey. It is the same shape the wordmark uses (the name, then its rule), so a
450
466
  * section of a report and the front door of the tool are drawn by one idea.
451
467
  */
452
- export function section(title, c, { width: total = COLUMNS } = {}) {
468
+ export function section(title, c, { width: total = outputColumns() } = {}) {
453
469
  return [c("heading", title), c("punctuation", DENSITY.floor.repeat(total))]
454
470
  }
455
471
 
456
472
  /** A rule with no heading, the same floor, for a wordmark or a block that names itself. */
457
- export function rule(c, { width: total = COLUMNS, tint = "punctuation" } = {}) {
473
+ export function rule(c, { width: total = outputColumns(), tint = "punctuation" } = {}) {
458
474
  return c(tint, DENSITY.floor.repeat(total))
459
475
  }
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { UNAUTHENTICATED_LIMIT } from "./github.mjs"
9
9
  import { MARKETPLACE_PIN } from "./pin.mjs"
10
- import { colourEnabled, paintProse, STEP, styler } from "./style.mjs"
10
+ import { colourEnabled, paintProse, STEP, styler, withOutputStream, wrap } from "./style.mjs"
11
11
 
12
12
  /**
13
13
  * "Safe" means one thing, everywhere it appears: this runs on your own
@@ -208,16 +208,40 @@ export function paintSignature(signature, c) {
208
208
  *
209
209
  * @param {{ colour?: boolean, heading?: boolean }} [options]
210
210
  */
211
- export function renderSummary({ colour = colourEnabled(), heading = true } = {}) {
212
- const c = styler(colour)
213
- const out = heading ? [`${c("typeable.bold", "omakit")}${c("punctuation", ":")} ${TAGLINE}`, ""] : []
214
- for (const command of COMMANDS) {
215
- out.push(`${INDENT}${paintSignature([].concat(command.signature)[0], c)}`)
211
+ function commandSignatures(command) {
212
+ const signatures = []
213
+ for (const line of [].concat(command.signature)) {
214
+ if (line.startsWith("omakit ")) signatures.push(line)
215
+ else signatures[signatures.length - 1] += ` ${line}`
216
216
  }
217
- out.push("")
218
- out.push(`${INDENT}${paintProse("`omakit help` is the same list with what each command does, and", c)}`)
219
- out.push(`${INDENT}${paintProse("what it reads. `omakit setup` is the one to run first.", c)}`)
220
- return `${out.join("\n")}\n`
217
+ return signatures
218
+ }
219
+
220
+ function signatureLines(signature, c) {
221
+ // A placeholder or bracketed optional argument stays whole, like the
222
+ // backticked typeable spans used by the shared wrapper.
223
+ const grouped = signature.replace(/\[[^\]]+\]|<[^>]+>(?:@<[^>]+>)?/g, (part) => `\`${part}\``)
224
+ return wrap(grouped, { indent: STEP * 3, first: STEP }).map((line) => paintSignature(line, c))
225
+ }
226
+
227
+ export function renderSummary({ stream = process.stdout, colour = colourEnabled(stream), heading = true } = {}) {
228
+ return withOutputStream(stream, () => {
229
+ const c = styler(colour)
230
+ const out = heading ? [`${c("typeable.bold", "omakit")}${c("punctuation", ":")} ${TAGLINE}`, ""] : []
231
+ if (stream.isTTY && heading) out.splice(0, 1, ...wrap(out[0]))
232
+ for (const command of COMMANDS) {
233
+ if (stream.isTTY) out.push(...signatureLines([].concat(command.signature)[0], c))
234
+ else out.push(`${INDENT}${paintSignature([].concat(command.signature)[0], c)}`)
235
+ }
236
+ out.push("")
237
+ if (stream.isTTY) {
238
+ out.push(...wrap("`omakit help` is the same list with what each command does, and what it reads. `omakit setup` is the one to run first.", { indent: STEP }, c))
239
+ } else {
240
+ out.push(`${INDENT}${paintProse("`omakit help` is the same list with what each command does, and", c)}`)
241
+ out.push(`${INDENT}${paintProse("what it reads. `omakit setup` is the one to run first.", c)}`)
242
+ }
243
+ return `${out.join("\n")}\n`
244
+ })
221
245
  }
222
246
 
223
247
  /**
@@ -225,23 +249,29 @@ export function renderSummary({ colour = colourEnabled(), heading = true } = {})
225
249
  * when the line above has already named the tool, as under an unknown
226
250
  * command, so the same sentence is not printed twice.
227
251
  */
228
- export function renderUsage({ colour = colourEnabled(), heading = true } = {}) {
229
- const c = styler(colour)
230
- const out = heading ? [`${c("typeable.bold", "omakit")}${c("punctuation", ":")} ${TAGLINE}`, ""] : []
252
+ export function renderUsage({ stream = process.stdout, colour = colourEnabled(stream), heading = true } = {}) {
253
+ return withOutputStream(stream, () => {
254
+ const c = styler(colour)
255
+ const out = heading ? [`${c("typeable.bold", "omakit")}${c("punctuation", ":")} ${TAGLINE}`, ""] : []
256
+ if (stream.isTTY && heading) out.splice(0, 1, ...wrap(out[0]))
231
257
 
232
- for (const command of COMMANDS) {
233
- for (const line of [].concat(command.signature)) {
234
- out.push(`${INDENT}${paintSignature(line, c)}`)
235
- }
236
- for (const line of command.lines) {
237
- out.push(`${DESCRIPTION}${paintProse(line, c)}`)
258
+ for (const command of COMMANDS) {
259
+ if (stream.isTTY) {
260
+ for (const signature of commandSignatures(command)) out.push(...signatureLines(signature, c))
261
+ out.push(...wrap(command.lines.join(" "), { indent: STEP * 3 }, c))
262
+ } else {
263
+ for (const line of [].concat(command.signature)) out.push(`${INDENT}${paintSignature(line, c)}`)
264
+ for (const line of command.lines) out.push(`${DESCRIPTION}${paintProse(line, c)}`)
265
+ }
266
+ out.push("")
238
267
  }
239
- out.push("")
240
- }
241
268
 
242
- out.push(`${INDENT}${paintSignature(TARGET_NOTE, c)}`)
243
- out.push("")
244
- out.push(c("heading", "GitHub access:"))
245
- for (const line of AUTHENTICATION) out.push(`${INDENT}${paintProse(line, c)}`)
246
- return `${out.join("\n")}\n`
269
+ if (stream.isTTY) out.push(...signatureLines(TARGET_NOTE, c))
270
+ else out.push(`${INDENT}${paintSignature(TARGET_NOTE, c)}`)
271
+ out.push("")
272
+ out.push(c("heading", "GitHub access:"))
273
+ if (stream.isTTY) out.push(...wrap(AUTHENTICATION.join(" "), { indent: STEP }, c))
274
+ else for (const line of AUTHENTICATION) out.push(`${INDENT}${paintProse(line, c)}`)
275
+ return `${out.join("\n")}\n`
276
+ })
247
277
  }
@@ -5,7 +5,7 @@
5
5
  // typed here is written anywhere.
6
6
 
7
7
  import { createInterface } from "node:readline"
8
- import { action, colourEnabled, COLUMNS, styler } from "../marketplace/style.mjs"
8
+ import { action, colourEnabled, outputColumns, styler } from "../marketplace/style.mjs"
9
9
 
10
10
  /**
11
11
  * The question as it is written to the terminal: the arrow line, wrapped
@@ -20,7 +20,7 @@ import { action, colourEnabled, COLUMNS, styler } from "../marketplace/style.mjs
20
20
  * @param {{ width?: number }} [options]
21
21
  * @returns {string} every line, joined, ending in `[y/N]: `
22
22
  */
23
- export function renderQuestion(question, c, { width = COLUMNS } = {}) {
23
+ export function renderQuestion(question, c, { width = outputColumns() } = {}) {
24
24
  return `${action(`${question} [y/N]:`, c, { indent: 0, width }).join("\n")} `
25
25
  }
26
26
 
@@ -28,7 +28,7 @@ export function renderQuestion(question, c, { width = COLUMNS } = {}) {
28
28
  * @param {{ input?: NodeJS.ReadStream, output?: NodeJS.WriteStream, colour?: boolean, question: string, width?: number }} options
29
29
  * @returns {Promise<boolean>} true only for `y` or `yes`, in any case; an empty line and the end of stdin are no
30
30
  */
31
- export function askYes({ input = process.stdin, output = process.stderr, colour = colourEnabled(output), question, width = COLUMNS }) {
31
+ export function askYes({ input = process.stdin, output = process.stderr, colour = colourEnabled(output), question, width = outputColumns(output) }) {
32
32
  const c = styler(colour)
33
33
  return new Promise((resolve) => {
34
34
  const rl = createInterface({ input, terminal: false })