claudeup 5.0.0 → 6.0.0
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/package.json +4 -4
- package/src/__tests__/cli-ansi.test.ts +297 -0
- package/src/__tests__/cli-apply-seams.test.ts +233 -0
- package/src/__tests__/cli-live.test.ts +384 -0
- package/src/__tests__/cli-update-view.test.ts +286 -0
- package/src/__tests__/dotenv.test.ts +5 -0
- package/src/__tests__/gitignore-fixer.test.ts +20 -1
- package/src/__tests__/marketplace-refresh.test.ts +63 -0
- package/src/cli/ansi.ts +512 -0
- package/src/cli/bootstrap.ts +0 -6
- package/src/cli/install.ts +4 -1
- package/src/cli/live.ts +374 -0
- package/src/cli/profile.ts +1 -1
- package/src/cli/router.ts +7 -3
- package/src/cli/update-view.ts +438 -0
- package/src/cli/update.ts +457 -129
- package/src/services/doctor-bins.ts +9 -1
- package/src/services/dotenv.ts +9 -4
- package/src/services/marketplace-refresh.ts +39 -1
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* update-view.ts — every string `claudeup update` prints, and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions: plan in, lines out. No IO, no timers, no terminal. That is
|
|
5
|
+
* what makes the look testable — the old report was `console.log` calls braided
|
|
6
|
+
* through the control flow, so the only way to check a column lined up was to
|
|
7
|
+
* run the command against a real machine.
|
|
8
|
+
*
|
|
9
|
+
* ## The visual contract
|
|
10
|
+
*
|
|
11
|
+
* Three rules, applied mechanically rather than case by case:
|
|
12
|
+
*
|
|
13
|
+
* 1. **A discrete status is a badge**, not a coloured word: dark-or-white ink
|
|
14
|
+
* on a saturated fill, so it reads as a chip at a glance.
|
|
15
|
+
* 2. **A bounded value is a bar**, not a numeral. A step's share of the run's
|
|
16
|
+
* wall clock and the apply loop's completion are both bars; the number
|
|
17
|
+
* labels the bar rather than replacing it.
|
|
18
|
+
* 3. **Dim the chrome, saturate the signal.** A plugin already at the right
|
|
19
|
+
* version is chrome — grey, no chip. Everything that needs a decision or a
|
|
20
|
+
* download is bright. Nine grey rows and three coloured ones is the whole
|
|
21
|
+
* point: the eye lands on the three.
|
|
22
|
+
*
|
|
23
|
+
* Colour never carries meaning alone. Every action has its own glyph and its
|
|
24
|
+
* own word, so the report survives NO_COLOR, a pipe, and colour blindness.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type {
|
|
28
|
+
BinUpdateItem,
|
|
29
|
+
PluginUpdateItem,
|
|
30
|
+
SkillUpdateItem,
|
|
31
|
+
UpdatePlan,
|
|
32
|
+
} from "../services/update-plan.js";
|
|
33
|
+
import { binInstallCommand, binUpgradeCommand } from "../services/toolchain.js";
|
|
34
|
+
import { brand } from "../ui/theme.js";
|
|
35
|
+
import {
|
|
36
|
+
badge,
|
|
37
|
+
bold,
|
|
38
|
+
dim,
|
|
39
|
+
elapsed,
|
|
40
|
+
fg,
|
|
41
|
+
meter,
|
|
42
|
+
padEnd,
|
|
43
|
+
padStart,
|
|
44
|
+
ramps,
|
|
45
|
+
spinner,
|
|
46
|
+
stackedBar,
|
|
47
|
+
sweep,
|
|
48
|
+
width,
|
|
49
|
+
} from "./ansi.js";
|
|
50
|
+
|
|
51
|
+
/** Two spaces of gutter on every line, so the report reads as one block. */
|
|
52
|
+
const PAD = " ";
|
|
53
|
+
/** Bars are this wide everywhere; a bar that changes width cannot be compared. */
|
|
54
|
+
const BAR = 18;
|
|
55
|
+
|
|
56
|
+
// -- action vocabulary -------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The single source of truth for what an action looks like.
|
|
60
|
+
*
|
|
61
|
+
* Glyph, word and hue together — defined once so a plugin `install` and a
|
|
62
|
+
* binary `install` cannot drift into two different blues with two different
|
|
63
|
+
* symbols, which is exactly what happened when each row built its own string.
|
|
64
|
+
*/
|
|
65
|
+
interface ActionStyle {
|
|
66
|
+
glyph: string;
|
|
67
|
+
label: string;
|
|
68
|
+
color: string;
|
|
69
|
+
/** Chrome rows are dimmed whole and carry no chip. */
|
|
70
|
+
chrome?: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Every action any of the three planners can produce, in one union. */
|
|
74
|
+
type ActionKey =
|
|
75
|
+
| "current"
|
|
76
|
+
| "install"
|
|
77
|
+
| "update"
|
|
78
|
+
| "upgrade"
|
|
79
|
+
| "repair"
|
|
80
|
+
| "refresh"
|
|
81
|
+
| "unknown";
|
|
82
|
+
|
|
83
|
+
const ACTIONS: Record<ActionKey, ActionStyle> = {
|
|
84
|
+
current: { glyph: "=", label: "CURRENT", color: brand.muted, chrome: true },
|
|
85
|
+
install: { glyph: "+", label: "INSTALL", color: brand.link },
|
|
86
|
+
update: { glyph: "↑", label: "UPDATE", color: brand.warning },
|
|
87
|
+
upgrade: { glyph: "↑", label: "UPGRADE", color: brand.warning },
|
|
88
|
+
repair: { glyph: "⟳", label: "REPAIR", color: brand.info },
|
|
89
|
+
refresh: { glyph: "⟳", label: "REFRESH", color: brand.info },
|
|
90
|
+
unknown: { glyph: "?", label: "UNKNOWN", color: brand.danger },
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** Widest label plus the badge's own two spaces of padding. */
|
|
94
|
+
const CHIP_WIDTH =
|
|
95
|
+
Math.max(...Object.values(ACTIONS).map((a) => a.label.length)) + 2;
|
|
96
|
+
|
|
97
|
+
/** A chip for an actionable row; flat dim text for a chrome one. */
|
|
98
|
+
function chip(key: ActionKey): string {
|
|
99
|
+
const style = ACTIONS[key];
|
|
100
|
+
const text = style.chrome
|
|
101
|
+
? dim(padEnd(` ${style.label} `, CHIP_WIDTH))
|
|
102
|
+
: badge(style.label, style.color);
|
|
103
|
+
return padEnd(text, CHIP_WIDTH);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function glyph(key: ActionKey): string {
|
|
107
|
+
return fg(ACTIONS[key].color, ACTIONS[key].glyph);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Bright for a row that needs something, grey for a row that does not. */
|
|
111
|
+
function rowText(key: ActionKey, text: string): string {
|
|
112
|
+
return ACTIONS[key].chrome ? dim(text) : text;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// -- header ------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
export function header(profileId: string): string[] {
|
|
118
|
+
return [
|
|
119
|
+
"",
|
|
120
|
+
`${PAD}${fg(brand.accent, "◆")} ${bold("claudeup update")} ${dim(
|
|
121
|
+
"profile",
|
|
122
|
+
)} ${fg(brand.accent, profileId)}`,
|
|
123
|
+
"",
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// -- phase checklist ---------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
export type StepState = "pending" | "running" | "done" | "warn" | "fail";
|
|
130
|
+
|
|
131
|
+
export interface Step {
|
|
132
|
+
label: string;
|
|
133
|
+
state: StepState;
|
|
134
|
+
/** Right-hand summary. While running, say what it is blocked on. */
|
|
135
|
+
detail: string;
|
|
136
|
+
startedAt?: number;
|
|
137
|
+
endedAt?: number;
|
|
138
|
+
/**
|
|
139
|
+
* Countable work, when the step has any.
|
|
140
|
+
*
|
|
141
|
+
* Present → a determinate meter. Absent → an indeterminate sweep, which is
|
|
142
|
+
* the honest widget for a call with no denominator. Never fake a percentage
|
|
143
|
+
* for one: a bar that creeps to 90% and stops is worse than no bar.
|
|
144
|
+
*/
|
|
145
|
+
progress?: { done: number; total: number };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const STEP_MARK: Record<Exclude<StepState, "running">, [string, string]> = {
|
|
149
|
+
pending: ["·", brand.muted],
|
|
150
|
+
done: ["✓", brand.success],
|
|
151
|
+
warn: ["⚠", brand.warning],
|
|
152
|
+
fail: ["✗", brand.danger],
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
function stepMark(step: Step, tick: number): string {
|
|
156
|
+
if (step.state === "running") return fg(brand.accent, spinner(tick));
|
|
157
|
+
const [mark, color] = STEP_MARK[step.state];
|
|
158
|
+
return fg(color, mark);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function stepDuration(step: Step, now: number): number | null {
|
|
162
|
+
if (step.startedAt === undefined) return null;
|
|
163
|
+
return (step.endedAt ?? now) - step.startedAt;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const STEP_LABEL_WIDTH = 14;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The animated frame: one row per step, spinner on whichever is running.
|
|
170
|
+
*
|
|
171
|
+
* Deliberately shows the pending steps too. A single spinner says "busy"; a
|
|
172
|
+
* four-row checklist with one spinner says "busy, on step two of four, and here
|
|
173
|
+
* is what is left" — which is the difference between a hang and progress.
|
|
174
|
+
*/
|
|
175
|
+
export function stepFrame(steps: Step[], tick: number, now: number): string[] {
|
|
176
|
+
return steps.map((step) => {
|
|
177
|
+
const ms = stepDuration(step, now);
|
|
178
|
+
const time = ms === null ? "" : dim(padStart(elapsed(ms), 6));
|
|
179
|
+
const label =
|
|
180
|
+
step.state === "pending"
|
|
181
|
+
? dim(padEnd(step.label, STEP_LABEL_WIDTH))
|
|
182
|
+
: padEnd(step.label, STEP_LABEL_WIDTH);
|
|
183
|
+
// Only the running row carries a bar. A finished row's bar belongs in the
|
|
184
|
+
// record below, where every step is on one scale and comparable; drawing
|
|
185
|
+
// it here too would put two competing bar columns on the same screen.
|
|
186
|
+
const bar =
|
|
187
|
+
step.state !== "running"
|
|
188
|
+
? " ".repeat(BAR)
|
|
189
|
+
: step.progress && step.progress.total > 0
|
|
190
|
+
? meter((step.progress.done / step.progress.total) * 100, BAR)
|
|
191
|
+
: sweep(BAR, tick, brand.accent);
|
|
192
|
+
// The bar carries the shape; this labels it. Without the count a half-full
|
|
193
|
+
// meter says "some of it", which is not the same information as "2 of 4".
|
|
194
|
+
const count =
|
|
195
|
+
step.state === "running" && step.progress && step.progress.total > 0
|
|
196
|
+
? `${fg(brand.accent, `${step.progress.done}/${step.progress.total}`)} `
|
|
197
|
+
: "";
|
|
198
|
+
const detail =
|
|
199
|
+
step.state === "running" ? fg(brand.accent, step.detail) : dim(step.detail);
|
|
200
|
+
return `${PAD}${stepMark(step, tick)} ${label} ${bar} ${time} ${count}${detail}`;
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The permanent record, with a duration bar per step.
|
|
206
|
+
*
|
|
207
|
+
* The bar is each step's share of the SLOWEST step, not of the total — the
|
|
208
|
+
* question a reader has when an update felt slow is "which part was it?", and
|
|
209
|
+
* shares of a total flatten to a uniform smear once there are four of them.
|
|
210
|
+
* Colour ramps green to red with that share, so the offender is the red one.
|
|
211
|
+
*/
|
|
212
|
+
export function stepRecord(steps: Step[], now: number): string[] {
|
|
213
|
+
const times = steps.map((s) => stepDuration(s, now) ?? 0);
|
|
214
|
+
const slowest = Math.max(1, ...times);
|
|
215
|
+
return steps.map((step, i) => {
|
|
216
|
+
const share = (times[i]! / slowest) * 100;
|
|
217
|
+
const bar = meter(share, BAR, ramps.severity);
|
|
218
|
+
const label = padEnd(step.label, STEP_LABEL_WIDTH);
|
|
219
|
+
const time = dim(padStart(elapsed(times[i]!), 6));
|
|
220
|
+
return `${PAD}${stepMark(step, 0)} ${label} ${bar} ${time} ${dim(
|
|
221
|
+
step.detail,
|
|
222
|
+
)}`;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// -- the plan ----------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
function pluginAction(item: PluginUpdateItem): ActionKey {
|
|
229
|
+
return item.action;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** What the row says on its right: versions, never the action word again. */
|
|
233
|
+
function pluginDetail(item: PluginUpdateItem): string {
|
|
234
|
+
const note = item.note ? ` ${dim(`(${item.note})`)}` : "";
|
|
235
|
+
switch (item.action) {
|
|
236
|
+
case "install":
|
|
237
|
+
return `${item.target ?? "latest"}${note}`;
|
|
238
|
+
case "update":
|
|
239
|
+
return `${dim(item.installed ?? "—")} ${fg(
|
|
240
|
+
brand.warning,
|
|
241
|
+
"→",
|
|
242
|
+
)} ${fg(brand.warning, item.target ?? "?")}${note}`;
|
|
243
|
+
case "repair":
|
|
244
|
+
case "unknown":
|
|
245
|
+
return `${item.installed ?? "—"}${note}`;
|
|
246
|
+
case "current":
|
|
247
|
+
return `${item.installed ?? "—"}${note}`;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function binDetail(item: BinUpdateItem, plan: UpdatePlan): string {
|
|
252
|
+
const spec = plan.binSpecs.get(item.name);
|
|
253
|
+
if (item.action === "current" || !spec)
|
|
254
|
+
return `pinned ${item.version ?? "—"}`;
|
|
255
|
+
const cmd =
|
|
256
|
+
item.action === "install"
|
|
257
|
+
? binInstallCommand(spec)
|
|
258
|
+
: binUpgradeCommand(spec);
|
|
259
|
+
return dim(cmd);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* A section: a stacked bar of the action mix, then one row per item.
|
|
264
|
+
*
|
|
265
|
+
* The bar is the section's whole story in one line — a reader who takes in
|
|
266
|
+
* nothing else knows whether this section is mostly settled or mostly work.
|
|
267
|
+
*/
|
|
268
|
+
function section(
|
|
269
|
+
title: string,
|
|
270
|
+
rows: Array<{ action: ActionKey; name: string; detail: string }>,
|
|
271
|
+
nameWidth: number,
|
|
272
|
+
): string[] {
|
|
273
|
+
if (rows.length === 0) return [];
|
|
274
|
+
const counts = new Map<ActionKey, number>();
|
|
275
|
+
for (const row of rows) counts.set(row.action, (counts.get(row.action) ?? 0) + 1);
|
|
276
|
+
const bar = stackedBar(
|
|
277
|
+
BAR,
|
|
278
|
+
[...counts].map(([action, value]) => ({
|
|
279
|
+
value,
|
|
280
|
+
color: ACTIONS[action].color,
|
|
281
|
+
})),
|
|
282
|
+
);
|
|
283
|
+
const legend = [...counts]
|
|
284
|
+
.map(([action, n]) => fg(ACTIONS[action].color, `${n} ${ACTIONS[action].label.toLowerCase()}`))
|
|
285
|
+
.join(dim(" · "));
|
|
286
|
+
|
|
287
|
+
return [
|
|
288
|
+
"",
|
|
289
|
+
`${PAD}${bold(padEnd(title, 9))} ${bar} ${dim(
|
|
290
|
+
padStart(String(rows.length), 3),
|
|
291
|
+
)} ${legend}`,
|
|
292
|
+
"",
|
|
293
|
+
...rows.map(
|
|
294
|
+
(row) =>
|
|
295
|
+
`${PAD}${glyph(row.action)} ${rowText(
|
|
296
|
+
row.action,
|
|
297
|
+
padEnd(row.name, nameWidth),
|
|
298
|
+
)} ${chip(row.action)} ${row.detail}`,
|
|
299
|
+
),
|
|
300
|
+
];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function planReport(plan: UpdatePlan): string[] {
|
|
304
|
+
const nameWidth = Math.max(
|
|
305
|
+
12,
|
|
306
|
+
...plan.plugins.map((p) => width(p.pluginId)),
|
|
307
|
+
...plan.bins.map((b) => width(b.name)),
|
|
308
|
+
...plan.skills.map((s) => width(s.name)),
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
return [
|
|
312
|
+
...section(
|
|
313
|
+
"Plugins",
|
|
314
|
+
plan.plugins.map((item) => ({
|
|
315
|
+
action: pluginAction(item),
|
|
316
|
+
name: item.pluginId,
|
|
317
|
+
detail: pluginDetail(item),
|
|
318
|
+
})),
|
|
319
|
+
nameWidth,
|
|
320
|
+
),
|
|
321
|
+
...section(
|
|
322
|
+
"CLI",
|
|
323
|
+
plan.bins.map((item: BinUpdateItem) => ({
|
|
324
|
+
action: item.action as ActionKey,
|
|
325
|
+
name: item.name,
|
|
326
|
+
detail: binDetail(item, plan),
|
|
327
|
+
})),
|
|
328
|
+
nameWidth,
|
|
329
|
+
),
|
|
330
|
+
...section(
|
|
331
|
+
"Skills",
|
|
332
|
+
plan.skills.map((item: SkillUpdateItem) => ({
|
|
333
|
+
action: item.action as ActionKey,
|
|
334
|
+
name: item.name,
|
|
335
|
+
detail: dim(`${item.ref.repo}/${item.ref.path}`),
|
|
336
|
+
})),
|
|
337
|
+
nameWidth,
|
|
338
|
+
),
|
|
339
|
+
];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// -- apply -------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
export interface ApplyState {
|
|
345
|
+
/** Items finished, of `total`. */
|
|
346
|
+
done: number;
|
|
347
|
+
total: number;
|
|
348
|
+
/** What is being worked on right now. */
|
|
349
|
+
current: string;
|
|
350
|
+
startedAt: number;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* The animated apply frame: a gradient meter that turns green as it fills.
|
|
355
|
+
*
|
|
356
|
+
* The meter's ramp encodes POSITION along the fill, which is what a smooth
|
|
357
|
+
* gradient is for — the count beside it is the label, not the signal.
|
|
358
|
+
*/
|
|
359
|
+
export function applyFrame(
|
|
360
|
+
state: ApplyState,
|
|
361
|
+
tick: number,
|
|
362
|
+
now: number,
|
|
363
|
+
): string[] {
|
|
364
|
+
const pct = state.total === 0 ? 100 : (state.done / state.total) * 100;
|
|
365
|
+
const count = `${state.done}/${state.total}`;
|
|
366
|
+
return [
|
|
367
|
+
"",
|
|
368
|
+
`${PAD}${fg(brand.accent, spinner(tick))} ${meter(pct, BAR * 2)} ${padStart(
|
|
369
|
+
count,
|
|
370
|
+
7,
|
|
371
|
+
)} ${fg(brand.accent, state.current)} ${dim(
|
|
372
|
+
elapsed(now - state.startedAt),
|
|
373
|
+
)}`,
|
|
374
|
+
];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** One finished item, written permanently above the meter. */
|
|
378
|
+
export function applyRow(
|
|
379
|
+
ok: boolean,
|
|
380
|
+
name: string,
|
|
381
|
+
nameWidth: number,
|
|
382
|
+
detail: string,
|
|
383
|
+
ms: number,
|
|
384
|
+
): string {
|
|
385
|
+
const mark = ok ? fg(brand.success, "✓") : fg(brand.danger, "✗");
|
|
386
|
+
return `${PAD}${mark} ${padEnd(name, nameWidth)} ${detail} ${dim(
|
|
387
|
+
padStart(elapsed(ms), 7),
|
|
388
|
+
)}`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Closing line: the run's shape as a bar, then the counts that label it. */
|
|
392
|
+
export function summaryLine(
|
|
393
|
+
changed: number,
|
|
394
|
+
current: number,
|
|
395
|
+
unknown: number,
|
|
396
|
+
failed: number,
|
|
397
|
+
ms: number,
|
|
398
|
+
): string[] {
|
|
399
|
+
const bar = stackedBar(BAR * 2, [
|
|
400
|
+
{ value: changed, color: brand.success },
|
|
401
|
+
{ value: current, color: brand.muted },
|
|
402
|
+
{ value: unknown, color: brand.warning },
|
|
403
|
+
{ value: failed, color: brand.danger },
|
|
404
|
+
]);
|
|
405
|
+
const parts = [
|
|
406
|
+
changed > 0 ? fg(brand.success, `${changed} changed`) : "",
|
|
407
|
+
current > 0 ? dim(`${current} current`) : "",
|
|
408
|
+
unknown > 0 ? fg(brand.warning, `${unknown} unverified`) : "",
|
|
409
|
+
failed > 0 ? fg(brand.danger, `${failed} failed`) : "",
|
|
410
|
+
].filter(Boolean);
|
|
411
|
+
return [
|
|
412
|
+
"",
|
|
413
|
+
`${PAD}${bar} ${parts.join(dim(" · "))} ${dim(elapsed(ms))}`,
|
|
414
|
+
"",
|
|
415
|
+
];
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// -- one-off notices ---------------------------------------------------------
|
|
419
|
+
|
|
420
|
+
export function ok(text: string): string {
|
|
421
|
+
return `${PAD}${fg(brand.success, "✓")} ${text}`;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function warn(text: string): string {
|
|
425
|
+
return `${PAD}${fg(brand.warning, "⚠")} ${text}`;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function fail(text: string): string {
|
|
429
|
+
return `${PAD}${fg(brand.danger, "✗")} ${text}`;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function info(text: string): string {
|
|
433
|
+
return `${PAD}${fg(brand.info, "›")} ${text}`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function note(text: string): string {
|
|
437
|
+
return `${PAD} ${dim(text)}`;
|
|
438
|
+
}
|