@sous-io/sous 0.2.37 → 1.0.2
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 +14 -256
- package/dist/bin.js +11 -0
- package/dist/sous.js +1094 -0
- package/docs/markdown/README.md +5 -4
- package/docs/markdown/_sidebar.md +21 -1
- package/docs/markdown/commands.md +294 -431
- package/docs/markdown/config-discovery.md +25 -14
- package/docs/markdown/config-inspection.md +53 -31
- package/docs/markdown/config-layers.md +168 -66
- package/docs/markdown/config-variables.md +44 -11
- package/docs/markdown/configuration.md +104 -17
- package/docs/markdown/design-principles.md +4 -1
- package/docs/markdown/repositories-authoring.md +30 -0
- package/docs/markdown/repositories-consuming.md +72 -14
- package/docs/markdown/repositories-file-formats.md +31 -13
- package/docs/markdown/repositories-providers.md +48 -57
- package/docs/markdown/repositories-quickstart.md +3 -2
- package/docs/markdown/repositories-troubleshooting.md +27 -1
- package/docs/markdown/repositories-variables.md +6 -1
- package/docs/markdown/repositories.md +18 -8
- package/package.json +48 -107
- package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +4 -6
- package/recipes/core/sous-skills/skills/about-liquid-templates/references/liquid-filters.tpl.md +2 -0
- package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +10 -9
- package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +35 -14
- package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +51 -20
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/sous.config.schema.json +351 -132
- package/bin/run.js +0 -51
- package/src/base-command.ts +0 -457
- package/src/commands/build.ts +0 -181
- package/src/commands/clear.ts +0 -79
- package/src/commands/compile.ts +0 -155
- package/src/commands/config/get.ts +0 -110
- package/src/commands/config/show.ts +0 -32
- package/src/commands/config/validate.ts +0 -53
- package/src/commands/help.ts +0 -46
- package/src/commands/init.ts +0 -289
- package/src/commands/launch.ts +0 -139
- package/src/commands/lock/rebuild.ts +0 -239
- package/src/commands/lock/show.ts +0 -115
- package/src/commands/namespace/list.ts +0 -138
- package/src/commands/namespace/show.ts +0 -130
- package/src/commands/prune.ts +0 -43
- package/src/commands/recipe/list.ts +0 -112
- package/src/commands/recipe/show.ts +0 -439
- package/src/commands/repo/add.ts +0 -145
- package/src/commands/repo/contribute.ts +0 -604
- package/src/commands/repo/gc.ts +0 -173
- package/src/commands/repo/init.ts +0 -136
- package/src/commands/repo/link.ts +0 -899
- package/src/commands/repo/list.ts +0 -236
- package/src/commands/repo/release.ts +0 -685
- package/src/commands/repo/remove.ts +0 -194
- package/src/commands/repo/search.ts +0 -243
- package/src/commands/repo/submit.ts +0 -341
- package/src/commands/repo/unlink.ts +0 -461
- package/src/commands/subscription/add.ts +0 -286
- package/src/commands/subscription/list.ts +0 -208
- package/src/commands/subscription/remove.ts +0 -182
- package/src/commands/subscription/update.ts +0 -216
- package/src/commands/update.ts +0 -178
- package/src/commands/vars/ask.ts +0 -417
- package/src/commands/vars/index.ts +0 -79
- package/src/commands/vars/list.ts +0 -67
- package/src/commands/vars/show.ts +0 -77
- package/src/config-command.ts +0 -30
- package/src/lib/build-preparation.ts +0 -128
- package/src/lib/build-service.ts +0 -580
- package/src/lib/config-discovery.ts +0 -378
- package/src/lib/config-inspect.ts +0 -145
- package/src/lib/config-kernel.mjs +0 -379
- package/src/lib/config-schema.ts +0 -401
- package/src/lib/env-file.ts +0 -328
- package/src/lib/env-local.ts +0 -212
- package/src/lib/errors.ts +0 -32
- package/src/lib/glob-pattern.ts +0 -24
- package/src/lib/include-resolver.ts +0 -523
- package/src/lib/interactive.ts +0 -165
- package/src/lib/markdown-compiler.ts +0 -947
- package/src/lib/package-info.ts +0 -25
- package/src/lib/pid-service.ts +0 -99
- package/src/lib/project-install.d.mts +0 -44
- package/src/lib/project-install.mjs +0 -234
- package/src/lib/project-scaffold/index.ts +0 -332
- package/src/lib/project-scaffold/templates.ts +0 -240
- package/src/lib/repos/catalog-display.ts +0 -215
- package/src/lib/repos/catalog-inputs.ts +0 -288
- package/src/lib/repos/catalog.ts +0 -842
- package/src/lib/repos/contribute.ts +0 -452
- package/src/lib/repos/core-recipe.ts +0 -105
- package/src/lib/repos/declarations.ts +0 -187
- package/src/lib/repos/defaults.ts +0 -175
- package/src/lib/repos/formats/common.ts +0 -440
- package/src/lib/repos/formats/index-file.ts +0 -253
- package/src/lib/repos/formats/links-map.ts +0 -98
- package/src/lib/repos/formats/lockfile.ts +0 -169
- package/src/lib/repos/formats/patterns.ts +0 -57
- package/src/lib/repos/formats/recipe-manifest.ts +0 -205
- package/src/lib/repos/formats/repo-manifest.ts +0 -97
- package/src/lib/repos/formats/store-entry.ts +0 -84
- package/src/lib/repos/formats/variable-definition.ts +0 -268
- package/src/lib/repos/freshness.ts +0 -264
- package/src/lib/repos/git-clone.ts +0 -850
- package/src/lib/repos/identity.ts +0 -89
- package/src/lib/repos/index.ts +0 -57
- package/src/lib/repos/links.ts +0 -356
- package/src/lib/repos/load-manifest.ts +0 -237
- package/src/lib/repos/lock-service.ts +0 -454
- package/src/lib/repos/locked-namespace-resolver.ts +0 -232
- package/src/lib/repos/locked-recipe-lookup.ts +0 -123
- package/src/lib/repos/locked-recipes.ts +0 -254
- package/src/lib/repos/managed-layer.ts +0 -422
- package/src/lib/repos/memories-hash-name.ts +0 -33
- package/src/lib/repos/namespace-resolver.ts +0 -527
- package/src/lib/repos/providers/base.ts +0 -268
- package/src/lib/repos/providers/git.ts +0 -245
- package/src/lib/repos/providers/github.ts +0 -589
- package/src/lib/repos/providers/gitlab.ts +0 -332
- package/src/lib/repos/providers/http.ts +0 -128
- package/src/lib/repos/providers/index-cache.ts +0 -421
- package/src/lib/repos/providers/index.ts +0 -106
- package/src/lib/repos/providers/local.ts +0 -410
- package/src/lib/repos/providers/provider.ts +0 -615
- package/src/lib/repos/recipe-config-layers.ts +0 -316
- package/src/lib/repos/recipe-key-matcher.ts +0 -62
- package/src/lib/repos/recipe-memories.ts +0 -178
- package/src/lib/repos/recipe-targets.ts +0 -236
- package/src/lib/repos/ref-search.ts +0 -46
- package/src/lib/repos/release/bump.ts +0 -161
- package/src/lib/repos/release/changelog.ts +0 -461
- package/src/lib/repos/release/git-state.ts +0 -460
- package/src/lib/repos/release/index-builder.ts +0 -1108
- package/src/lib/repos/release/index.ts +0 -19
- package/src/lib/repos/release/plan.ts +0 -521
- package/src/lib/repos/release/settle.ts +0 -154
- package/src/lib/repos/release/submissions.ts +0 -214
- package/src/lib/repos/release/submit-checkout.ts +0 -290
- package/src/lib/repos/release/submit-questions.ts +0 -153
- package/src/lib/repos/release/submit-service.ts +0 -898
- package/src/lib/repos/release/tags.ts +0 -239
- package/src/lib/repos/release/validate.ts +0 -463
- package/src/lib/repos/resolver.ts +0 -1028
- package/src/lib/repos/scaffold/index.ts +0 -245
- package/src/lib/repos/scaffold/templates.ts +0 -517
- package/src/lib/repos/seed.ts +0 -473
- package/src/lib/repos/store/contract.ts +0 -64
- package/src/lib/repos/store/hash.ts +0 -191
- package/src/lib/repos/store/recipe-store.ts +0 -599
- package/src/lib/repos/store/settings.ts +0 -58
- package/src/lib/repos/subscription-service.ts +0 -3859
- package/src/lib/repos/trust.ts +0 -447
- package/src/lib/repos/update-plan.ts +0 -235
- package/src/lib/self-update/index.ts +0 -14
- package/src/lib/self-update/installs.ts +0 -413
- package/src/lib/self-update/managers.ts +0 -126
- package/src/lib/self-update/plan.ts +0 -315
- package/src/lib/self-update/registry.ts +0 -99
- package/src/lib/self-update/run.ts +0 -506
- package/src/lib/self-update/versions.ts +0 -243
- package/src/lib/settings.ts +0 -1107
- package/src/lib/sous-home.ts +0 -104
- package/src/lib/state.ts +0 -167
- package/src/lib/vars/answers.ts +0 -192
- package/src/lib/vars/ask.ts +0 -1152
- package/src/lib/vars/definition-source.ts +0 -261
- package/src/lib/vars/display.ts +0 -231
- package/src/lib/vars/index.ts +0 -20
- package/src/lib/vars/ladder.ts +0 -282
- package/src/lib/vars/mappings.ts +0 -260
- package/src/lib/vars/names.ts +0 -94
- package/src/lib/vars/preanswers.ts +0 -395
- package/src/lib/vars/question-plan.ts +0 -241
- package/src/lib/vars/report.ts +0 -228
- package/src/lib/vars/safe-regex.ts +0 -235
- package/src/lib/vars/secrets.ts +0 -131
- package/src/lib/vars/validate.ts +0 -312
- package/src/lib/version-report.ts +0 -90
- package/src/lib/watch-loop.ts +0 -148
- package/src/lib/watch-service.ts +0 -116
- package/src/services/ref-resolver/container.ts +0 -107
- package/src/services/ref-resolver/dependency.ts +0 -60
- package/src/services/ref-resolver/format.ts +0 -197
- package/src/services/ref-resolver/glob.ts +0 -115
- package/src/services/ref-resolver/hash-names.ts +0 -155
- package/src/services/ref-resolver/index.ts +0 -55
- package/src/services/ref-resolver/injectable.ts +0 -36
- package/src/services/ref-resolver/location.ts +0 -62
- package/src/services/ref-resolver/lookups/cached-index-lookup.ts +0 -53
- package/src/services/ref-resolver/lookups/catalog-lookup.ts +0 -23
- package/src/services/ref-resolver/lookups/catalog-matcher.ts +0 -261
- package/src/services/ref-resolver/lookups/chained-lookup.ts +0 -27
- package/src/services/ref-resolver/lookups/env-var-lookup.ts +0 -65
- package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +0 -150
- package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +0 -62
- package/src/services/ref-resolver/lookups/ref-lookup.ts +0 -54
- package/src/services/ref-resolver/lookups/variable-lookup.ts +0 -123
- package/src/services/ref-resolver/named-refs.ts +0 -144
- package/src/services/ref-resolver/parser/env-var-splitter.ts +0 -27
- package/src/services/ref-resolver/parser/location-splitter.ts +0 -239
- package/src/services/ref-resolver/parser/name-path-splitter.ts +0 -200
- package/src/services/ref-resolver/parser/partial-ref.ts +0 -84
- package/src/services/ref-resolver/parser/query-splitter.ts +0 -61
- package/src/services/ref-resolver/parser/range-splitter.ts +0 -52
- package/src/services/ref-resolver/parser/ref-parser.ts +0 -115
- package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +0 -51
- package/src/services/ref-resolver/parts.ts +0 -115
- package/src/services/ref-resolver/pruners/command-line-pruner.ts +0 -47
- package/src/services/ref-resolver/pruners/include-pruner.ts +0 -66
- package/src/services/ref-resolver/pruners/manifest-pruner.ts +0 -130
- package/src/services/ref-resolver/pruners/ref-pruner.ts +0 -135
- package/src/services/ref-resolver/pruners/rule-helpers.ts +0 -44
- package/src/services/ref-resolver/pruners/stored-key-pruner.ts +0 -136
- package/src/services/ref-resolver/ref-helpers.ts +0 -62
- package/src/services/ref-resolver/ref-picker-service.ts +0 -204
- package/src/services/ref-resolver/ref-report.ts +0 -84
- package/src/services/ref-resolver/ref-resolve-arguments.ts +0 -47
- package/src/services/ref-resolver/ref-resolve-result.ts +0 -36
- package/src/services/ref-resolver/ref-resolver-service.ts +0 -323
- package/src/services/ref-resolver/source.ts +0 -48
- package/src/services/ref-resolver/tokens.ts +0 -25
- package/src/services/ref-resolver/types.ts +0 -155
- package/src/templating/filters/bullet-list.ts +0 -9
- package/src/templating/filters/index.ts +0 -8
- package/src/templating/init-liquid-engine.ts +0 -139
- package/src/templating/lib/glob-files.ts +0 -74
- package/src/templating/lib/import-export.ts +0 -32
- package/src/templating/lib/secret-scope.ts +0 -41
- package/src/templating/lib/tag-args.ts +0 -19
- package/src/templating/tags/exportScalarVarsJs.ts +0 -46
- package/src/templating/tags/getFiles.ts +0 -89
- package/src/templating/tags/index.ts +0 -14
- package/src/templating/tags/listFiles.ts +0 -54
- package/src/templating/tags/showVars.ts +0 -28
- package/src/utils/choice-prompt.ts +0 -143
- package/src/utils/command-errors.ts +0 -186
- package/src/utils/command-help.ts +0 -45
- package/src/utils/confirm-prompt.ts +0 -110
- package/src/utils/flags.ts +0 -177
- package/src/utils/formatting.ts +0 -849
- package/src/utils/prompts.ts +0 -53
- package/src/utils/sous-directory.ts +0 -245
- package/src/utils/table.ts +0 -603
- package/src/utils/value-prompt.ts +0 -119
package/src/utils/formatting.ts
DELETED
|
@@ -1,849 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { color } from "@oclif/color";
|
|
3
|
-
|
|
4
|
-
const HEADER_LINES = [
|
|
5
|
-
" ▄█████ ▄████▄ ██ ██ ▄█████ ",
|
|
6
|
-
" ▀▀▀▄▄▄ ██ ██ ██ ██ ▀▀▀▄▄▄ ",
|
|
7
|
-
" █████▀ ▀████▀ ▀████▀ █████▀ ",
|
|
8
|
-
];
|
|
9
|
-
|
|
10
|
-
// --- The Color Palette ---------------------------------------------------------------------------
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Every color sous writes comes from this one place, so a reader learns the
|
|
14
|
-
* vocabulary once: a label is cyan, the value beside it is bright white, an
|
|
15
|
-
* aside about that value is grey, a warning is bright yellow with its sharpest
|
|
16
|
-
* words in orange, an explanation is bright teal, and an error is bright red.
|
|
17
|
-
*
|
|
18
|
-
* The orange is given as a hex value on purpose. A terminal that can show it
|
|
19
|
-
* does; one limited to sixteen colors has chalk fold it down to bright yellow,
|
|
20
|
-
* which keeps a highlighted word inside the warning's own color rather than
|
|
21
|
-
* turning it into something that reads as a second kind of message.
|
|
22
|
-
*/
|
|
23
|
-
export const palette = {
|
|
24
|
-
/** The name in a key and value pair. */
|
|
25
|
-
label: (text: string): string => color.cyan(text),
|
|
26
|
-
/** The value beside a label. */
|
|
27
|
-
value: (text: string): string => color.whiteBright(text),
|
|
28
|
-
/** A trailing aside: a location, a provenance, anything secondary. */
|
|
29
|
-
muted: (text: string): string => color.gray(text),
|
|
30
|
-
/** The body of a warning. */
|
|
31
|
-
warning: (text: string): string => color.yellowBright(text),
|
|
32
|
-
/** The words inside a warning that carry the actual risk. */
|
|
33
|
-
highlight: (text: string): string => color.hex("#ff8800")(text),
|
|
34
|
-
/** An explanatory line that is neither a warning nor an error. */
|
|
35
|
-
note: (text: string): string => color.cyanBright(text),
|
|
36
|
-
/** The body of an error. */
|
|
37
|
-
error: (text: string): string => color.redBright(text),
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* The prefix every error message carries, so an error is still findable by
|
|
42
|
-
* eye or by grep when the output has no color at all (a pipe, a log file,
|
|
43
|
-
* `CI=true`).
|
|
44
|
-
*/
|
|
45
|
-
export const ERROR_PREFIX = "Error: ";
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Adds the error prefix to a message that does not already begin with one, so
|
|
49
|
-
* a message that says "Error:" itself is never made to say it twice.
|
|
50
|
-
*
|
|
51
|
-
* @param text - The first line of an error message.
|
|
52
|
-
*/
|
|
53
|
-
export function withErrorPrefix(text: string): string {
|
|
54
|
-
return /^error:\s/i.test(text.trimStart()) ? text : `${ERROR_PREFIX}${text}`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// --- Core Output Functions -----------------------------------------------------------------------
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Writes a string of text to the console, with a newline.
|
|
62
|
-
*/
|
|
63
|
-
export function log(text: string): void {
|
|
64
|
-
console.log(text);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Writes a string of text to the console. Concurrent calls to this
|
|
69
|
-
* function will replace the text of the previous call, making this
|
|
70
|
-
* function useful for things like progress indicators.
|
|
71
|
-
*/
|
|
72
|
-
export function logPersistent(text: string): void {
|
|
73
|
-
process.stdout.write(`\r${text}`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// --- Whitespace Helpers --------------------------------------------------------------------------
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Writes a blank line to the console.
|
|
80
|
-
* Note that we output a space character to ensure that the line is not
|
|
81
|
-
* completely empty, which some terminal emulators will ignore.
|
|
82
|
-
*/
|
|
83
|
-
export function blankLine(): void {
|
|
84
|
-
log(" ");
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Writes multiple blank lines to the console.
|
|
89
|
-
*/
|
|
90
|
-
export function blankLines(blankLineCount = 2): void {
|
|
91
|
-
for (let i = 0; i < blankLineCount; i++) {
|
|
92
|
-
blankLine();
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// --- String Builders -----------------------------------------------------------------------------
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Applies a prefix symbol to a string of text. If `prefixSymbol` is
|
|
100
|
-
* an empty string, the text is returned verbatim.
|
|
101
|
-
*/
|
|
102
|
-
function applyPrefixSymbol(text: string, prefixSymbol = "➔"): string {
|
|
103
|
-
const prefix = prefixSymbol ? `${prefixSymbol} ` : "";
|
|
104
|
-
return `${prefix}${text}`;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Formats a heading text string. If the heading does not end with a
|
|
109
|
-
* period, a colon is appended.
|
|
110
|
-
*/
|
|
111
|
-
function formatHeadingText(heading: string): string {
|
|
112
|
-
const append = heading.endsWith(".") ? "" : ":";
|
|
113
|
-
return `${heading}${append}`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Indents a block of text by a specified number of spaces.
|
|
118
|
-
*/
|
|
119
|
-
export function indent(text: string, count = 2, char = " "): string {
|
|
120
|
-
return text
|
|
121
|
-
.split("\n")
|
|
122
|
-
.map(line => char.repeat(count) + line)
|
|
123
|
-
.join("\n");
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// --- Header & Footer -----------------------------------------------------------------------------
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Writes the CLI header using the given line writer.
|
|
130
|
-
*
|
|
131
|
-
* Factored out so commands whose stdout must stay machine-readable (the
|
|
132
|
-
* `sous config *` commands, which emit JSON) can route the decorative banner to
|
|
133
|
-
* stderr instead — see ConfigCommand.emitHeader.
|
|
134
|
-
*
|
|
135
|
-
* @param write - Receives one already-formatted line at a time (no trailing newline).
|
|
136
|
-
*/
|
|
137
|
-
export function headerTo(write: (line: string) => void): void {
|
|
138
|
-
write(" ");
|
|
139
|
-
write(" ");
|
|
140
|
-
write(color.cyan(HEADER_LINES.join("\n")));
|
|
141
|
-
write(" Agent Configuration Manager ");
|
|
142
|
-
write(" ");
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* How many commands are running inside another one right now. While any are,
|
|
147
|
-
* `header` writes nothing, because the command that started them has already
|
|
148
|
-
* drawn the banner once.
|
|
149
|
-
*/
|
|
150
|
-
let nestedCommandDepth = 0;
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Writes the CLI header to stdout, unless the command asking for it runs
|
|
154
|
-
* inside another command (see `withoutHeader`).
|
|
155
|
-
*/
|
|
156
|
-
export function header(): void {
|
|
157
|
-
if (nestedCommandDepth > 0) return;
|
|
158
|
-
headerTo(log);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Runs another command in this process without drawing the CLI header a
|
|
163
|
-
* second time. A command that chains others (`sous repo contribute`) wraps each
|
|
164
|
-
* one in this, so the output opens with one banner however many run.
|
|
165
|
-
*
|
|
166
|
-
* @param run - Runs the nested command.
|
|
167
|
-
* @returns Whatever `run` resolves to.
|
|
168
|
-
*/
|
|
169
|
-
export async function withoutHeader<T>(run: () => Promise<T>): Promise<T> {
|
|
170
|
-
nestedCommandDepth += 1;
|
|
171
|
-
try {
|
|
172
|
-
return await run();
|
|
173
|
-
} finally {
|
|
174
|
-
nestedCommandDepth -= 1;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Writes the CLI footer to the console.
|
|
180
|
-
*/
|
|
181
|
-
export function footer(): void {
|
|
182
|
-
blankLines(2);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// --- Headings & Subheadings ----------------------------------------------------------------------
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Writes a heading to the console.
|
|
189
|
-
*
|
|
190
|
-
* @example
|
|
191
|
-
* heading("Compiling targets");
|
|
192
|
-
* // ▶ Compiling targets:
|
|
193
|
-
*/
|
|
194
|
-
export function heading(text: string, prefixSymbol = "▶"): void {
|
|
195
|
-
const formatted = formatHeadingText(text);
|
|
196
|
-
const final = applyPrefixSymbol(formatted, prefixSymbol);
|
|
197
|
-
blankLine();
|
|
198
|
-
log(color.yellowBright(final));
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Writes a section heading followed by a blank line, which is how a command
|
|
203
|
-
* opens a section of its output.
|
|
204
|
-
*
|
|
205
|
-
* `heading` on its own leaves the first line of content pressed right up under
|
|
206
|
-
* the heading, which reads as one crowded block. Every command that lays out
|
|
207
|
-
* sections of prose, tables or variable lists uses this instead, so the spacing
|
|
208
|
-
* is decided in one place rather than by a `blankLine()` call remembered at
|
|
209
|
-
* each site.
|
|
210
|
-
*
|
|
211
|
-
* @param text - The heading text; a colon is appended unless it ends in a period.
|
|
212
|
-
* @param prefixSymbol - The marker drawn before the text.
|
|
213
|
-
*
|
|
214
|
-
* @example
|
|
215
|
-
* section("Adding a repository");
|
|
216
|
-
* // ▶ Adding a repository:
|
|
217
|
-
* // (blank line)
|
|
218
|
-
*/
|
|
219
|
-
export function section(text: string, prefixSymbol = "▶"): void {
|
|
220
|
-
heading(text, prefixSymbol);
|
|
221
|
-
blankLine();
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Writes a subheading to the console.
|
|
226
|
-
*
|
|
227
|
-
* @example
|
|
228
|
-
* subheading("Target 1");
|
|
229
|
-
* // ➔ Target 1:
|
|
230
|
-
*/
|
|
231
|
-
export function subheading(text: string, prefixSymbol = "➔"): void {
|
|
232
|
-
const formatted = formatHeadingText(text);
|
|
233
|
-
const final = applyPrefixSymbol(formatted, prefixSymbol);
|
|
234
|
-
blankLine();
|
|
235
|
-
log(color.whiteBright(final));
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// --- Data Display Functions ----------------------------------------------------------------------
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Dumps the contents of an object to the console.
|
|
242
|
-
*/
|
|
243
|
-
export function dump<T>(obj: T, headingText?: string): void {
|
|
244
|
-
if (headingText) {
|
|
245
|
-
subheading(headingText);
|
|
246
|
-
}
|
|
247
|
-
const normalized =
|
|
248
|
-
typeof obj === "object" && obj !== null
|
|
249
|
-
? (sortObjectKeys(obj as Record<string, any>) as T)
|
|
250
|
-
: obj;
|
|
251
|
-
console.dir(normalized);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Displays a count of something.
|
|
256
|
-
*
|
|
257
|
-
* @example
|
|
258
|
-
* showCount(5, "tokens");
|
|
259
|
-
* // Found [5] tokens
|
|
260
|
-
*/
|
|
261
|
-
export function showCount(count: number, entity = "items", headingText = "", verb = "Found"): void {
|
|
262
|
-
if (headingText) {
|
|
263
|
-
subheading(headingText);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const prefix = verb ? `${verb} ` : "";
|
|
267
|
-
let strCount = count.toLocaleString();
|
|
268
|
-
strCount = color.whiteBright(strCount);
|
|
269
|
-
strCount = color.cyan("[") + strCount + color.cyan("]");
|
|
270
|
-
|
|
271
|
-
let line = `${prefix}${strCount} ${entity}`;
|
|
272
|
-
if (headingText) {
|
|
273
|
-
line = indent(line);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
log(line);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// --- The One Key and Value Display ---------------------------------------------------------------
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* How far a key and value block is indented. Every one of them sits at the same
|
|
283
|
-
* depth, whether it is a command's opening block, a result, or the facts about
|
|
284
|
-
* a variable.
|
|
285
|
-
*/
|
|
286
|
-
export const VARIABLE_INDENT = 4;
|
|
287
|
-
|
|
288
|
-
/** One key and value pair, as the display functions take it. */
|
|
289
|
-
export interface VariableEntry {
|
|
290
|
-
/**
|
|
291
|
-
* The name, written on the left. An empty label means "the same label as the
|
|
292
|
-
* line above, continued": the label column and its colon are left blank, so a
|
|
293
|
-
* fact that needs several lines still lines up under itself.
|
|
294
|
-
*/
|
|
295
|
-
label: string;
|
|
296
|
-
/** The value, written after the colon. Anything printable. */
|
|
297
|
-
value: unknown;
|
|
298
|
-
/**
|
|
299
|
-
* A secondary fact about the value: where it came from, where it lives. It
|
|
300
|
-
* follows the value in muted grey rather than in parentheses, so the value
|
|
301
|
-
* itself stays the thing the eye lands on.
|
|
302
|
-
*/
|
|
303
|
-
detail?: string;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/** How a key and value block is laid out. */
|
|
307
|
-
export interface ShowVariablesOptions {
|
|
308
|
-
/** How far the block is indented. Four by default. */
|
|
309
|
-
indent?: number;
|
|
310
|
-
/**
|
|
311
|
-
* The column the colons line up at, counted from the start of the label.
|
|
312
|
-
* Worked out from the longest label when it is not given, which is what
|
|
313
|
-
* `showVariables` does for a whole block.
|
|
314
|
-
*/
|
|
315
|
-
labelWidth?: number;
|
|
316
|
-
/** The column to wrap values at. Defaults to the wrap width. */
|
|
317
|
-
width?: number;
|
|
318
|
-
/** Where the lines go. The console by default. */
|
|
319
|
-
write?: (line: string) => void;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Lays one key and value pair out: the label in the label color, padded so
|
|
324
|
-
* every colon in the block lines up, then the value in the value color, then
|
|
325
|
-
* any detail in muted grey. A value too long for the line wraps and hangs under
|
|
326
|
-
* the value column rather than under the label.
|
|
327
|
-
*
|
|
328
|
-
* @param entry - The label, the value and any trailing detail.
|
|
329
|
-
* @param options - Indentation, label column and wrap column.
|
|
330
|
-
* @returns The rendered lines.
|
|
331
|
-
*/
|
|
332
|
-
export function formatVariable(
|
|
333
|
-
entry: VariableEntry,
|
|
334
|
-
options: ShowVariablesOptions = {},
|
|
335
|
-
): string[] {
|
|
336
|
-
const pad = " ".repeat(Math.max(0, options.indent ?? VARIABLE_INDENT));
|
|
337
|
-
const labelWidth = Math.max(options.labelWidth ?? entry.label.length, entry.label.length);
|
|
338
|
-
const gutter = labelWidth + 2; // the padded label, then ": "
|
|
339
|
-
const width = options.width ?? wrapColumns();
|
|
340
|
-
const valueWidth = Math.max(20, width - pad.length - gutter);
|
|
341
|
-
|
|
342
|
-
const valueText = entry.value === undefined || entry.value === null ? "" : String(entry.value);
|
|
343
|
-
const valueLines = wrapText(valueText, valueWidth, { hangingIndent: 0 });
|
|
344
|
-
const detailLines =
|
|
345
|
-
entry.detail === undefined || entry.detail === ""
|
|
346
|
-
? []
|
|
347
|
-
: wrapText(entry.detail, valueWidth, { hangingIndent: 0 });
|
|
348
|
-
|
|
349
|
-
// The detail joins the value's last line when both fit, and starts a line of
|
|
350
|
-
// its own when they do not, so a long location is never cut in half.
|
|
351
|
-
const cells: string[] = valueLines.map(line => palette.value(line));
|
|
352
|
-
if (detailLines.length > 0) {
|
|
353
|
-
const last = valueLines[valueLines.length - 1] ?? "";
|
|
354
|
-
const [first, ...rest] = detailLines;
|
|
355
|
-
if (last !== "" && displayWidth(last) + 1 + displayWidth(first ?? "") <= valueWidth) {
|
|
356
|
-
cells[cells.length - 1] = `${palette.value(last)} ${palette.muted(first ?? "")}`;
|
|
357
|
-
} else {
|
|
358
|
-
cells.push(palette.muted(first ?? ""));
|
|
359
|
-
}
|
|
360
|
-
for (const line of rest) cells.push(palette.muted(line));
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
if (cells.length === 0) cells.push("");
|
|
364
|
-
|
|
365
|
-
return cells.map((cell, index) =>
|
|
366
|
-
index === 0 && entry.label !== ""
|
|
367
|
-
? `${pad}${palette.label(entry.label.padEnd(labelWidth))}: ${cell}`
|
|
368
|
-
: `${pad}${" ".repeat(gutter)}${cell}`,
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Writes one key and value pair to the console.
|
|
374
|
-
*
|
|
375
|
-
* @param label - The name, written on the left.
|
|
376
|
-
* @param value - The value, written after the colon.
|
|
377
|
-
* @param options - Indentation, label column, wrap column and where to write.
|
|
378
|
-
*
|
|
379
|
-
* @example
|
|
380
|
-
* showVariable("Config", "./my-config.js");
|
|
381
|
-
* // Config: ./my-config.js
|
|
382
|
-
*/
|
|
383
|
-
export function showVariable(
|
|
384
|
-
label: string,
|
|
385
|
-
value: unknown,
|
|
386
|
-
options: ShowVariablesOptions & { detail?: string } = {},
|
|
387
|
-
): void {
|
|
388
|
-
const write = options.write ?? log;
|
|
389
|
-
const entry: VariableEntry = {
|
|
390
|
-
label,
|
|
391
|
-
value,
|
|
392
|
-
...(options.detail === undefined ? {} : { detail: options.detail }),
|
|
393
|
-
};
|
|
394
|
-
for (const line of formatVariable(entry, options)) write(line);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Writes a list of key and value pairs to the console, with the colons lined
|
|
399
|
-
* up. Every key and value display in sous goes through this function or through
|
|
400
|
-
* `showVariable`, so there is exactly one of them to change.
|
|
401
|
-
*
|
|
402
|
-
* @param entries - The pairs, either as a plain object or as a list carrying details.
|
|
403
|
-
* @param options - Indentation, wrap column and where to write.
|
|
404
|
-
*
|
|
405
|
-
* @example
|
|
406
|
-
* showVariables({ Config: "./my-config.js", Strict: "false" });
|
|
407
|
-
* // Config: ./my-config.js
|
|
408
|
-
* // Strict: false
|
|
409
|
-
*/
|
|
410
|
-
export function showVariables(
|
|
411
|
-
entries: Record<string, unknown> | VariableEntry[],
|
|
412
|
-
options: ShowVariablesOptions = {},
|
|
413
|
-
): void {
|
|
414
|
-
const list: VariableEntry[] = Array.isArray(entries)
|
|
415
|
-
? entries
|
|
416
|
-
: Object.entries(entries).map(([label, value]) => ({ label, value }));
|
|
417
|
-
if (list.length === 0) return;
|
|
418
|
-
|
|
419
|
-
const labelWidth =
|
|
420
|
-
options.labelWidth ?? Math.max(...list.map(entry => entry.label.length));
|
|
421
|
-
for (const entry of list) showVariable(entry.label, entry.value, {
|
|
422
|
-
...options,
|
|
423
|
-
labelWidth,
|
|
424
|
-
...(entry.detail === undefined ? {} : { detail: entry.detail }),
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* Displays a heading labelled "Command Variables" followed by a variable list.
|
|
430
|
-
*
|
|
431
|
-
* The "Dry Run" entry is a special case: it is noise when dry-run mode is off, so it
|
|
432
|
-
* is only shown when its value is `true`. Every other entry is always shown.
|
|
433
|
-
*/
|
|
434
|
-
export function showCommandVars(vars: Record<string, any>): void {
|
|
435
|
-
const filtered = Object.fromEntries(
|
|
436
|
-
Object.entries(vars).filter(([name, value]) => name !== "Dry Run" || value === true),
|
|
437
|
-
);
|
|
438
|
-
subheading("Command Variables", "$");
|
|
439
|
-
showVariables(filtered);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* Displays the status of an in-progress scan using an in-place log line.
|
|
444
|
-
*/
|
|
445
|
-
export function scanStatus(matchedRecordCount: number, totalScanCount: number): void {
|
|
446
|
-
const displayMatched = `[${color.white(matchedRecordCount.toLocaleString())}]`;
|
|
447
|
-
const displayScanned = `[${color.white(totalScanCount.toLocaleString())}]`;
|
|
448
|
-
logPersistent(
|
|
449
|
-
color.gray(
|
|
450
|
-
`Scan in progress: ${displayMatched} records matched (so far) of ${displayScanned} records scanned ...`,
|
|
451
|
-
),
|
|
452
|
-
);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* Displays the status of an in-progress deletion using an in-place log line.
|
|
457
|
-
*/
|
|
458
|
-
export function deleteStatus(recordsDeleted: number, totalRecordsToDelete: number): void {
|
|
459
|
-
const displayDeleted = `[${color.white(recordsDeleted.toLocaleString())}]`;
|
|
460
|
-
const displayTotal = `[${color.white(totalRecordsToDelete.toLocaleString())}]`;
|
|
461
|
-
logPersistent(
|
|
462
|
-
color.gray(
|
|
463
|
-
`Deletion in progress: ${displayDeleted} of ${displayTotal} records deleted ...`,
|
|
464
|
-
),
|
|
465
|
-
);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// --- Special Notices -----------------------------------------------------------------------------
|
|
469
|
-
|
|
470
|
-
/**
|
|
471
|
-
* Writes an error message to the console in red.
|
|
472
|
-
*
|
|
473
|
-
* @param text - The message to display.
|
|
474
|
-
* @param write - Line sink (default stdout via `log`). Commands whose stdout must
|
|
475
|
-
* stay machine-readable (the `sous config *` JSON commands) pass a stderr writer
|
|
476
|
-
* so error text never corrupts a piped stdout stream.
|
|
477
|
-
*/
|
|
478
|
-
export function displayError(text: string, write: (line: string) => void = log): void {
|
|
479
|
-
const lines = text.split("\n").filter(line => line.trim() !== "");
|
|
480
|
-
write("");
|
|
481
|
-
lines.forEach((line, index) => {
|
|
482
|
-
const body = index === 0 ? withErrorPrefix(line.trim()) : line.trim();
|
|
483
|
-
for (const wrapped of wrapText(body, wrapColumns() - 2)) {
|
|
484
|
-
write(indent(palette.error(wrapped)));
|
|
485
|
-
}
|
|
486
|
-
});
|
|
487
|
-
write("");
|
|
488
|
-
write("");
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
* Writes a pre-formatted, multi-line error message to the console in red,
|
|
493
|
-
* preserving the message's own indentation and blank lines.
|
|
494
|
-
*
|
|
495
|
-
* Use this instead of `displayError` when the message contains deliberate
|
|
496
|
-
* structure (a checked-paths list, a code sample, numbered steps). `displayError`
|
|
497
|
-
* trims every line and drops blanks, which flattens that structure.
|
|
498
|
-
*
|
|
499
|
-
* @param text - The pre-formatted, multi-line message to display.
|
|
500
|
-
* @param write - Line sink (default stdout via `log`). Commands whose stdout must
|
|
501
|
-
* stay machine-readable (the `sous config *` JSON commands) pass a stderr writer
|
|
502
|
-
* so error text never corrupts a piped stdout stream.
|
|
503
|
-
*
|
|
504
|
-
* @example
|
|
505
|
-
* displayErrorBlock("No config found.\n\n Checked:\n /a/.sous/");
|
|
506
|
-
*/
|
|
507
|
-
export function displayErrorBlock(text: string, write: (line: string) => void = log): void {
|
|
508
|
-
const lines = text.split("\n");
|
|
509
|
-
// The prefix goes on the first line that has any text on it, which is the
|
|
510
|
-
// line a reader (or a grep) looks at.
|
|
511
|
-
const first = lines.findIndex(line => line.trim() !== "");
|
|
512
|
-
|
|
513
|
-
write("");
|
|
514
|
-
lines.forEach((line, index) => {
|
|
515
|
-
if (line === "") {
|
|
516
|
-
write(" ");
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
519
|
-
const body = index === first ? withErrorPrefix(line) : line;
|
|
520
|
-
for (const wrapped of wrapText(body, wrapColumns() - 2)) {
|
|
521
|
-
write(indent(palette.error(wrapped)));
|
|
522
|
-
}
|
|
523
|
-
});
|
|
524
|
-
write("");
|
|
525
|
-
write("");
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
/**
|
|
529
|
-
* Writes a notice indicating that the operation is running in dry-run mode. It
|
|
530
|
-
* is an explanation of what is about to not happen, so it carries the note
|
|
531
|
-
* color rather than the warning one.
|
|
532
|
-
*
|
|
533
|
-
* @example
|
|
534
|
-
* dryRunNotice("File will not be written.");
|
|
535
|
-
* // [Dry Run] File will not be written.
|
|
536
|
-
*/
|
|
537
|
-
export function dryRunNotice(text: string): void {
|
|
538
|
-
note(`[Dry Run] ${text}`, { indent: 4 });
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
/**
|
|
542
|
-
* Writes a warning message to the console with a yellow banner. The body is
|
|
543
|
-
* bright yellow; words written in capitals inside it are taken as the ones
|
|
544
|
-
* carrying the risk and are drawn in orange, so a reader skimming the block
|
|
545
|
-
* still takes in the part that matters.
|
|
546
|
-
*
|
|
547
|
-
* @param text - The message to display.
|
|
548
|
-
* @param write - Line sink (default stdout via `log`). Commands whose stdout must
|
|
549
|
-
* stay machine-readable (the `sous config *` JSON commands) pass a stderr writer
|
|
550
|
-
* so warning text never corrupts a piped stdout stream.
|
|
551
|
-
*/
|
|
552
|
-
export function warning(text: string, write: (line: string) => void = log): void {
|
|
553
|
-
write("");
|
|
554
|
-
write("");
|
|
555
|
-
write(color.bgYellowBright(color.black(" WARNING: ")));
|
|
556
|
-
|
|
557
|
-
for (const line of text.split("\n")) {
|
|
558
|
-
if (line.trim() === "") {
|
|
559
|
-
write(" ");
|
|
560
|
-
continue;
|
|
561
|
-
}
|
|
562
|
-
for (const wrapped of wrapText(line.trim(), wrapColumns() - 2)) {
|
|
563
|
-
write(indent(palette.warning(highlightUpperCaseWords(wrapped))));
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
write("");
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// --- Miscellaneous Helpers -----------------------------------------------------------------------
|
|
570
|
-
|
|
571
|
-
/**
|
|
572
|
-
* Highlights all UPPERCASE words in the given string.
|
|
573
|
-
*/
|
|
574
|
-
function highlightUpperCaseWords(
|
|
575
|
-
str: string,
|
|
576
|
-
highlightFn: (word: string) => string = palette.highlight,
|
|
577
|
-
): string {
|
|
578
|
-
return str.replace(/\b[A-Z][A-Z_'"()\[\]{}<>|&*!@#%^\\-]+\b/g, match => highlightFn(match));
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
/**
|
|
582
|
-
* Sorts the keys of an object alphabetically.
|
|
583
|
-
*/
|
|
584
|
-
export function sortObjectKeys<T extends Record<string, any>>(obj: T): T {
|
|
585
|
-
if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
|
|
586
|
-
return obj;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
try {
|
|
590
|
-
return Object.keys(obj)
|
|
591
|
-
.sort()
|
|
592
|
-
.reduce((acc, key) => {
|
|
593
|
-
// @ts-ignore
|
|
594
|
-
acc[key] = obj[key];
|
|
595
|
-
return acc;
|
|
596
|
-
}, {} as T);
|
|
597
|
-
} catch {
|
|
598
|
-
/* c8 ignore next */
|
|
599
|
-
return obj;
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// --- ANSI-Safe Measurement -----------------------------------------------------------------------
|
|
604
|
-
|
|
605
|
-
/**
|
|
606
|
-
* Matches the SGR (Select Graphic Rendition) escape sequences the coloring
|
|
607
|
-
* helpers emit, which is every escape sequence sous writes into a string.
|
|
608
|
-
*/
|
|
609
|
-
const SGR_PATTERN = /\u001B\[[0-9;]*m/g;
|
|
610
|
-
|
|
611
|
-
/**
|
|
612
|
-
* Removes the color escape sequences from a string, leaving the characters a
|
|
613
|
-
* reader actually sees.
|
|
614
|
-
*
|
|
615
|
-
* @param text - The text to strip.
|
|
616
|
-
* @returns The same text, without any color escape sequence.
|
|
617
|
-
*/
|
|
618
|
-
export function stripAnsi(text: string): string {
|
|
619
|
-
return String(text ?? "").replace(SGR_PATTERN, "");
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
/**
|
|
623
|
-
* How many terminal columns a string occupies once its color codes are taken
|
|
624
|
-
* out. Every alignment decision measures with this, because a colored cell is
|
|
625
|
-
* longer than it looks.
|
|
626
|
-
*
|
|
627
|
-
* East Asian wide characters and emoji are out of scope: this counts one column
|
|
628
|
-
* per code unit, so a cell holding them aligns a little short. Everything sous
|
|
629
|
-
* lays out in a table is an identifier, a path, a URL or English prose, so the
|
|
630
|
-
* simple measure holds; widening it later means changing this one function.
|
|
631
|
-
*
|
|
632
|
-
* @param text - The text to measure.
|
|
633
|
-
* @returns The visible width, in columns.
|
|
634
|
-
*/
|
|
635
|
-
export function displayWidth(text: string): number {
|
|
636
|
-
return stripAnsi(text).length;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// --- Width-Aware Wrapping ------------------------------------------------------------------------
|
|
640
|
-
|
|
641
|
-
/**
|
|
642
|
-
* The width sous wraps prose to when the output is not a terminal, or when the
|
|
643
|
-
* terminal never said how wide it is.
|
|
644
|
-
*/
|
|
645
|
-
export const DEFAULT_WRAP_COLUMNS = 100;
|
|
646
|
-
|
|
647
|
-
/**
|
|
648
|
-
* How many columns the output has to work with: the real terminal width when
|
|
649
|
-
* there is a terminal, and the default width otherwise, so a piped or recorded
|
|
650
|
-
* run always wraps the same way.
|
|
651
|
-
*
|
|
652
|
-
* @param stream - The stream to measure. Defaults to stdout.
|
|
653
|
-
*/
|
|
654
|
-
export function terminalColumns(stream: { columns?: number } = process.stdout): number {
|
|
655
|
-
const columns = stream.columns;
|
|
656
|
-
return typeof columns === "number" && columns > 0 ? columns : DEFAULT_WRAP_COLUMNS;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
/**
|
|
660
|
-
* How far short of the right edge text stops, so a wrapped paragraph never
|
|
661
|
-
* touches the last column of the terminal.
|
|
662
|
-
*/
|
|
663
|
-
export const RIGHT_MARGIN = 2;
|
|
664
|
-
|
|
665
|
-
/**
|
|
666
|
-
* The width every paragraph sous prints is wrapped to: the columns available,
|
|
667
|
-
* less the right margin.
|
|
668
|
-
*
|
|
669
|
-
* @param stream - The stream to measure. Defaults to stdout.
|
|
670
|
-
*/
|
|
671
|
-
export function wrapColumns(stream: { columns?: number } = process.stdout): number {
|
|
672
|
-
return Math.max(20, terminalColumns(stream) - RIGHT_MARGIN);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
/** How a paragraph is wrapped. */
|
|
676
|
-
export interface WrapOptions {
|
|
677
|
-
/**
|
|
678
|
-
* How far a continuation line hangs past the first line of its paragraph.
|
|
679
|
-
* Two by default, which is what makes a wrapped sentence read as one
|
|
680
|
-
* paragraph rather than as several. Pass zero where the caller lays out its
|
|
681
|
-
* own hanging indent, as the labeled blocks do.
|
|
682
|
-
*/
|
|
683
|
-
hangingIndent?: number;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
/**
|
|
687
|
-
* Wraps a paragraph to a width, breaking on spaces and never inside a word. A
|
|
688
|
-
* word longer than the width (a URL, a path) is left whole on a line of its
|
|
689
|
-
* own, because half a URL is worse than a long line.
|
|
690
|
-
*
|
|
691
|
-
* Newlines already in the text are honored: each line is wrapped on its own,
|
|
692
|
-
* and each keeps whatever indentation it was written with, so an already laid
|
|
693
|
-
* out block survives being passed through. Continuation lines hang two spaces
|
|
694
|
-
* past the line they continue, unless the caller asks for something else.
|
|
695
|
-
*
|
|
696
|
-
* Width is measured with the color codes taken out, so a colored word is
|
|
697
|
-
* counted by what a reader sees rather than by what the escape sequences add.
|
|
698
|
-
*
|
|
699
|
-
* @param text - The prose to wrap.
|
|
700
|
-
* @param width - The column to wrap at. Defaults to the terminal width, less the right margin.
|
|
701
|
-
* @param options - How far continuation lines hang.
|
|
702
|
-
* @returns One string per rendered line, without trailing spaces.
|
|
703
|
-
*
|
|
704
|
-
* @example
|
|
705
|
-
* wrapText("one two three", 7, { hangingIndent: 0 });
|
|
706
|
-
* // -> ["one two", "three"]
|
|
707
|
-
*/
|
|
708
|
-
export function wrapText(
|
|
709
|
-
text: string,
|
|
710
|
-
width: number = wrapColumns(),
|
|
711
|
-
options: WrapOptions = {},
|
|
712
|
-
): string[] {
|
|
713
|
-
const limit = Math.max(1, Math.floor(width));
|
|
714
|
-
const hanging = Math.max(0, Math.floor(options.hangingIndent ?? 2));
|
|
715
|
-
const lines: string[] = [];
|
|
716
|
-
|
|
717
|
-
for (const paragraph of String(text ?? "").split("\n")) {
|
|
718
|
-
// The line's own indentation is kept, and continuation lines are measured
|
|
719
|
-
// and drawn from inside it, so a block that arrives already laid out is
|
|
720
|
-
// still laid out when it leaves.
|
|
721
|
-
const leading = /^[ \t]*/.exec(paragraph)?.[0] ?? "";
|
|
722
|
-
const body = paragraph.slice(leading.length);
|
|
723
|
-
const words = body.split(/\s+/).filter(word => word.length > 0);
|
|
724
|
-
if (words.length === 0) {
|
|
725
|
-
lines.push("");
|
|
726
|
-
continue;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
const continuation = leading + " ".repeat(hanging);
|
|
730
|
-
let prefix = leading;
|
|
731
|
-
let current = "";
|
|
732
|
-
|
|
733
|
-
for (const word of words) {
|
|
734
|
-
if (current.length === 0) {
|
|
735
|
-
current = word;
|
|
736
|
-
continue;
|
|
737
|
-
}
|
|
738
|
-
const projected = displayWidth(prefix) + displayWidth(current) + 1 + displayWidth(word);
|
|
739
|
-
if (projected <= limit) {
|
|
740
|
-
current = `${current} ${word}`;
|
|
741
|
-
continue;
|
|
742
|
-
}
|
|
743
|
-
lines.push(`${prefix}${current}`);
|
|
744
|
-
prefix = continuation;
|
|
745
|
-
current = word;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
lines.push(`${prefix}${current}`);
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
return lines;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
/** How a paragraph is laid out for printing. */
|
|
755
|
-
export interface ParagraphOptions extends WrapOptions {
|
|
756
|
-
/** How far the whole paragraph is indented. Two by default. */
|
|
757
|
-
indent?: number;
|
|
758
|
-
/** The column to wrap at, indentation included. Defaults to the wrap width. */
|
|
759
|
-
width?: number;
|
|
760
|
-
/** A color to paint every line with. */
|
|
761
|
-
color?: (text: string) => string;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
/**
|
|
765
|
-
* Lays a paragraph out for printing: wrapped to the width left after its own
|
|
766
|
-
* indentation, indented, and colored.
|
|
767
|
-
*
|
|
768
|
-
* @param text - The prose to lay out.
|
|
769
|
-
* @param options - Indentation, wrap column and color.
|
|
770
|
-
* @returns The rendered lines.
|
|
771
|
-
*/
|
|
772
|
-
export function formatParagraph(text: string, options: ParagraphOptions = {}): string[] {
|
|
773
|
-
const pad = Math.max(0, options.indent ?? 2);
|
|
774
|
-
const width = options.width ?? wrapColumns();
|
|
775
|
-
const wrapOptions: WrapOptions =
|
|
776
|
-
options.hangingIndent === undefined ? {} : { hangingIndent: options.hangingIndent };
|
|
777
|
-
const paint = options.color ?? ((line: string) => line);
|
|
778
|
-
|
|
779
|
-
return wrapText(text, Math.max(20, width - pad), wrapOptions).map(line =>
|
|
780
|
-
line === "" ? " " : " ".repeat(pad) + paint(line),
|
|
781
|
-
);
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
/**
|
|
785
|
-
* Writes a wrapped, indented paragraph to the console. Every long sentence the
|
|
786
|
-
* CLI prints goes through this, so nothing is ever left to the terminal's own
|
|
787
|
-
* idea of where a line should break.
|
|
788
|
-
*
|
|
789
|
-
* @param text - The prose to print.
|
|
790
|
-
* @param options - Indentation, wrap column and color.
|
|
791
|
-
*/
|
|
792
|
-
export function paragraph(text: string, options: ParagraphOptions = {}): void {
|
|
793
|
-
for (const line of formatParagraph(text, options)) log(line);
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
/**
|
|
797
|
-
* Writes an explanatory line: something that is neither a warning nor an error,
|
|
798
|
-
* but a note about what just happened or what is about to. It is bright teal,
|
|
799
|
-
* which is the one color reserved for explanation.
|
|
800
|
-
*
|
|
801
|
-
* @param text - The note to print.
|
|
802
|
-
* @param options - Indentation and wrap column.
|
|
803
|
-
*/
|
|
804
|
-
export function note(text: string, options: Omit<ParagraphOptions, "color"> = {}): void {
|
|
805
|
-
paragraph(text, { ...options, color: palette.note });
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
// --- Key Bindings --------------------------------------------------------------------------------
|
|
809
|
-
|
|
810
|
-
/**
|
|
811
|
-
* The one-line legend a prompt draws under itself, in the style the stock
|
|
812
|
-
* `@inquirer/select` prompt uses: the key in bold, what it does dimmed beside
|
|
813
|
-
* it, pairs separated by a dimmed bullet.
|
|
814
|
-
*
|
|
815
|
-
* @param keys - Key and action pairs, in the order they are shown.
|
|
816
|
-
* @returns The legend line.
|
|
817
|
-
*
|
|
818
|
-
* @example
|
|
819
|
-
* keysHelpTip([["↑↓", "navigate"], ["⏎", "select"]]);
|
|
820
|
-
* // -> "↑↓ navigate • ⏎ select"
|
|
821
|
-
*/
|
|
822
|
-
export function keysHelpTip(keys: Array<[key: string, action: string]>): string {
|
|
823
|
-
return keys
|
|
824
|
-
.map(([key, action]) => `${color.bold(key)} ${color.dim(action)}`)
|
|
825
|
-
.join(color.dim(" • "));
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
/**
|
|
829
|
-
* The character every bulleted line in the CLI is drawn with, so a list of
|
|
830
|
-
* points always looks like a list of points.
|
|
831
|
-
*/
|
|
832
|
-
export const BULLET = "•";
|
|
833
|
-
|
|
834
|
-
/** How many blank lines a question keeps under itself while it is waiting. */
|
|
835
|
-
export const PROMPT_BOTTOM_PADDING = 2;
|
|
836
|
-
|
|
837
|
-
/**
|
|
838
|
-
* The block a prompt draws under its input line: whatever it has to say
|
|
839
|
-
* (a validation message, the key legend), then two blank lines, so a question
|
|
840
|
-
* never sits on the terminal's very last row with its legend scrolled away.
|
|
841
|
-
*
|
|
842
|
-
* @param content - The line under the prompt, or an empty string for none.
|
|
843
|
-
* @returns The bottom block, ready to hand back from a prompt's renderer.
|
|
844
|
-
*/
|
|
845
|
-
export function promptBottom(content: string): string {
|
|
846
|
-
const lines = content === "" ? [] : [content];
|
|
847
|
-
for (let index = 0; index < PROMPT_BOTTOM_PADDING; index++) lines.push(" ");
|
|
848
|
-
return lines.join("\n");
|
|
849
|
-
}
|