ucode-agent 1.4.0 → 1.6.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/README.md +64 -4
- package/package.json +5 -2
- package/skills/build-app/SKILL.md +19 -1
- package/skills/ui-ux/SKILL.md +2 -2
- package/src/core/doctor.js +122 -0
- package/src/core/loop.js +303 -12
- package/src/core/provider.js +93 -10
- package/src/core/stuck.js +269 -0
- package/src/tools/browser.js +121 -59
- package/src/tools/deploy.js +283 -0
- package/src/tools/files.js +91 -8
- package/src/tools/index.js +65 -8
- package/src/tools/scaffold.js +190 -0
- package/src/tools/shell.js +89 -1
- package/src/ui/activity.js +203 -0
- package/src/ui/plain.js +22 -3
- package/src/ui/screen.js +65 -19
- package/templates/next-shadcn/AGENTS.md +9 -0
- package/templates/next-shadcn/README.md +9 -0
- package/templates/next-shadcn/TEMPLATE.md +83 -0
- package/templates/next-shadcn/_gitignore +41 -0
- package/templates/next-shadcn/_package-lock.json +11465 -0
- package/templates/next-shadcn/components.json +25 -0
- package/templates/next-shadcn/eslint.config.mjs +18 -0
- package/templates/next-shadcn/next-env.d.ts +5 -0
- package/templates/next-shadcn/next.config.ts +8 -0
- package/templates/next-shadcn/package.json +39 -0
- package/templates/next-shadcn/postcss.config.mjs +7 -0
- package/templates/next-shadcn/presets/citrus.json +77 -0
- package/templates/next-shadcn/presets/graphite.json +77 -0
- package/templates/next-shadcn/presets/grove.json +77 -0
- package/templates/next-shadcn/presets/ocean.json +78 -0
- package/templates/next-shadcn/presets/sunset.json +77 -0
- package/templates/next-shadcn/presets/violet.json +77 -0
- package/templates/next-shadcn/src/app/favicon.ico +0 -0
- package/templates/next-shadcn/src/app/globals.css +155 -0
- package/templates/next-shadcn/src/app/layout.tsx +42 -0
- package/templates/next-shadcn/src/app/page.tsx +14 -0
- package/templates/next-shadcn/src/components/theme-provider.tsx +7 -0
- package/templates/next-shadcn/src/components/theme-toggle.tsx +27 -0
- package/templates/next-shadcn/src/components/ui/accordion.tsx +80 -0
- package/templates/next-shadcn/src/components/ui/alert-dialog.tsx +199 -0
- package/templates/next-shadcn/src/components/ui/avatar.tsx +111 -0
- package/templates/next-shadcn/src/components/ui/badge.tsx +48 -0
- package/templates/next-shadcn/src/components/ui/button.tsx +66 -0
- package/templates/next-shadcn/src/components/ui/calendar.tsx +222 -0
- package/templates/next-shadcn/src/components/ui/card.tsx +102 -0
- package/templates/next-shadcn/src/components/ui/checkbox.tsx +32 -0
- package/templates/next-shadcn/src/components/ui/collapsible.tsx +33 -0
- package/templates/next-shadcn/src/components/ui/command.tsx +195 -0
- package/templates/next-shadcn/src/components/ui/dialog.tsx +168 -0
- package/templates/next-shadcn/src/components/ui/dropdown-menu.tsx +268 -0
- package/templates/next-shadcn/src/components/ui/hover-card.tsx +43 -0
- package/templates/next-shadcn/src/components/ui/input-group.tsx +156 -0
- package/templates/next-shadcn/src/components/ui/input.tsx +18 -0
- package/templates/next-shadcn/src/components/ui/label.tsx +23 -0
- package/templates/next-shadcn/src/components/ui/popover.tsx +88 -0
- package/templates/next-shadcn/src/components/ui/progress.tsx +30 -0
- package/templates/next-shadcn/src/components/ui/radio-group.tsx +43 -0
- package/templates/next-shadcn/src/components/ui/scroll-area.tsx +54 -0
- package/templates/next-shadcn/src/components/ui/select.tsx +191 -0
- package/templates/next-shadcn/src/components/ui/separator.tsx +27 -0
- package/templates/next-shadcn/src/components/ui/sheet.tsx +147 -0
- package/templates/next-shadcn/src/components/ui/skeleton.tsx +13 -0
- package/templates/next-shadcn/src/components/ui/slider.tsx +58 -0
- package/templates/next-shadcn/src/components/ui/sonner.tsx +49 -0
- package/templates/next-shadcn/src/components/ui/switch.tsx +32 -0
- package/templates/next-shadcn/src/components/ui/table.tsx +115 -0
- package/templates/next-shadcn/src/components/ui/tabs.tsx +89 -0
- package/templates/next-shadcn/src/components/ui/textarea.tsx +17 -0
- package/templates/next-shadcn/src/components/ui/toggle-group.tsx +89 -0
- package/templates/next-shadcn/src/components/ui/toggle.tsx +46 -0
- package/templates/next-shadcn/src/components/ui/tooltip.tsx +56 -0
- package/templates/next-shadcn/src/lib/utils.ts +6 -0
- package/templates/next-shadcn/tsconfig.json +34 -0
- package/ucode.js +8 -1
package/README.md
CHANGED
|
@@ -111,12 +111,69 @@ chosen model gets another go a few minutes later.
|
|
|
111
111
|
|
|
112
112
|
## What it does
|
|
113
113
|
|
|
114
|
-
**
|
|
115
|
-
`edit_file`, `multi_edit`, `edit_files`, `list_dir`, `glob`,
|
|
116
|
-
`run_command`, `run_commands`, `look_at_app`, `web_search`. Read-only calls run in parallel,
|
|
114
|
+
**Fifteen tools.** `create_app`, `read_file`, `read_files`, `write_file`,
|
|
115
|
+
`batch_write`, `edit_file`, `multi_edit`, `edit_files`, `list_dir`, `glob`,
|
|
116
|
+
`grep`, `run_command`, `run_commands`, `look_at_app`, `web_search`. Read-only calls run in parallel,
|
|
117
117
|
and start the moment the model finishes writing them — while the rest of its
|
|
118
118
|
reply is still arriving. Anything that writes runs on its own, in order.
|
|
119
119
|
|
|
120
|
+
**Apps start from a ready-made starter.** Setting up Next.js and shadcn from
|
|
121
|
+
nothing takes about four minutes — `create-next-app` and the shadcn CLI measured
|
|
122
|
+
at 116s and 130s — plus a dozen model round trips. `create_app` copies ucode's
|
|
123
|
+
starter instead: Next.js 16, TypeScript, Tailwind 4, shadcn/ui with 25 common
|
|
124
|
+
components, light/dark mode, toasts and a considered theme, already known to
|
|
125
|
+
build. The copy takes under a second, and its install runs in the background
|
|
126
|
+
while the model writes the first components.
|
|
127
|
+
|
|
128
|
+
**Built to be fast, and measured.** A traced build of a small Next.js app went
|
|
129
|
+
from 17 minutes and 116 model steps to about 6 minutes and 25 steps, by fixing
|
|
130
|
+
where the time actually went:
|
|
131
|
+
|
|
132
|
+
- Edits return the file as it now stands, so the model does not re-read it.
|
|
133
|
+
- Files written more than a few steps ago stop being re-sent in full; the
|
|
134
|
+
conversation stays small, so every step answers faster.
|
|
135
|
+
- A file-write whose JSON is malformed — a missing comma, an unescaped quote in
|
|
136
|
+
the code, raw line breaks — is repaired instead of thrown away with all its
|
|
137
|
+
output.
|
|
138
|
+
- Every write is parsed on the spot, so a syntax error comes back in the same
|
|
139
|
+
step rather than a minute later from a failed build.
|
|
140
|
+
- A failed build that is missing a component or package says exactly which
|
|
141
|
+
command fixes it.
|
|
142
|
+
- The starter is the shadcn models already know (Radix), so the code they write
|
|
143
|
+
compiles the first time.
|
|
144
|
+
|
|
145
|
+
`UCODE_TRACE=1` writes every model call and tool, with its duration, to
|
|
146
|
+
`~/.ucode/trace.jsonl`.
|
|
147
|
+
|
|
148
|
+
**Deploy in one line.** Say "deploy it", or type `/deploy [folder]`, and the app
|
|
149
|
+
goes live on Vercel. ucode picks a short project name that fits the app and is
|
|
150
|
+
free (`food-iq`, else `food-iq-app`…), copies the app's `.env` keys to Vercel as
|
|
151
|
+
encrypted variables, refuses code with a secret written into it (and says how to
|
|
152
|
+
move it to a server route), and gives you the link. Deploying again updates the
|
|
153
|
+
same link. Needs a token from vercel.com/account/tokens in `~/.ucode/.env` as
|
|
154
|
+
`VERCEL_TOKEN=...`.
|
|
155
|
+
|
|
156
|
+
**A look for every app.** `create_app` takes a design preset — ocean, grove,
|
|
157
|
+
sunset, graphite, violet or citrus — each a full light and dark palette with its
|
|
158
|
+
own font, so apps stop looking like the same default blue.
|
|
159
|
+
|
|
160
|
+
**It notices when it is going round in circles.** The same failing edit, an edit
|
|
161
|
+
that changes nothing, or a build failing on the same errors three times gets a
|
|
162
|
+
firm, specific note; if that does not work, the turn moves to another model.
|
|
163
|
+
|
|
164
|
+
**It never dies at the daily limit.** When the free daily limit runs out mid-build,
|
|
165
|
+
ucode counts down to the reset and carries on by itself.
|
|
166
|
+
|
|
167
|
+
**You can see it working.** The status row shows the current step with a light
|
|
168
|
+
sweeping across it, the step count and the time, and each answer ends with
|
|
169
|
+
`✓ Done in 6m 12s · 25 steps`. When a dev server comes up, the app opens in your
|
|
170
|
+
browser (`UCODE_OPEN=0` turns that off).
|
|
171
|
+
|
|
172
|
+
**`/stats` and `ucode doctor`.** `/stats` shows the session's time, steps, tokens,
|
|
173
|
+
files and builds. `ucode doctor` (or `/doctor`) checks Node, npm, git, the API
|
|
174
|
+
key and today's free requests left, the browser, the Vercel token and the
|
|
175
|
+
version, with the fix for anything wrong.
|
|
176
|
+
|
|
120
177
|
**Parallel workers.** When a build splits into parts that touch different files
|
|
121
178
|
— the API route, the upload component, the results view — the model hands them
|
|
122
179
|
to up to three workers that build at the same time, each line in the transcript
|
|
@@ -133,7 +190,10 @@ to download — at 375px and 1440px. It reports console errors, failed requests,
|
|
|
133
190
|
content that spills off a phone screen, broken images and unlabeled controls,
|
|
134
191
|
saves screenshots to `.ucode/screenshots`, and has Nemotron Nano Omni review them
|
|
135
192
|
the way a designer would. The model fixes what it finds before calling the app
|
|
136
|
-
done.
|
|
193
|
+
done. Both widths load at once, and the designer review — the slow part — runs
|
|
194
|
+
on the first look at an app in each request and is skipped, not waited on, when
|
|
195
|
+
the vision model is busy. The look after the fixes re-runs only the fast checks:
|
|
196
|
+
a few seconds.
|
|
137
197
|
|
|
138
198
|
**Errors fixed before you see them.** When the model says it is done, ucode
|
|
139
199
|
type-checks every file it changed — `tsc --noEmit` for TypeScript projects,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucode-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "ucode - a terminal coding agent that reads, edits and runs your code, on NVIDIA and Cohere models.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "ucode.js",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"ucode.js",
|
|
12
12
|
"src/",
|
|
13
|
-
"skills/"
|
|
13
|
+
"skills/",
|
|
14
|
+
"templates/"
|
|
14
15
|
],
|
|
15
16
|
"scripts": {
|
|
16
17
|
"start": "node ucode.js",
|
|
@@ -44,8 +45,10 @@
|
|
|
44
45
|
"author": "om dixit",
|
|
45
46
|
"license": "ISC",
|
|
46
47
|
"dependencies": {
|
|
48
|
+
"@babel/parser": "^7.29.8",
|
|
47
49
|
"chalk": "^6.0.0",
|
|
48
50
|
"dotenv": "^17.4.2",
|
|
51
|
+
"jsonrepair": "^3.15.0",
|
|
49
52
|
"marked": "^15.0.12",
|
|
50
53
|
"marked-terminal": "^7.3.0",
|
|
51
54
|
"openai": "^7.4.0",
|
|
@@ -31,7 +31,25 @@ If there is a user interface, the `ui-ux` skill is already loaded. Decide the
|
|
|
31
31
|
design direction now, not after the logic works. If the app calls a model,
|
|
32
32
|
load `ai-features`; if it has accounts, keys or uploads, load `security`.
|
|
33
33
|
|
|
34
|
-
## 2.
|
|
34
|
+
## 2. Start from the starter
|
|
35
|
+
|
|
36
|
+
**For a Next.js app, call `create_app`** — one step, about a second:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
create_app({ folder: "my-app", name: "My App", description: "…" })
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
It copies ucode's ready-made starter — Next.js 16, TypeScript, Tailwind 4,
|
|
43
|
+
shadcn/ui with 25 common components, light/dark mode, toasts, a considered
|
|
44
|
+
theme — which is already known to build, and starts `npm install` in the
|
|
45
|
+
background. Read the `TEMPLATE.md` it lists, then start writing components
|
|
46
|
+
straight away; commands in that folder wait for the install on their own.
|
|
47
|
+
Re-tint the palette in `globals.css` and swap the font for the app's direction.
|
|
48
|
+
|
|
49
|
+
Never run `create-next-app` or `shadcn init` for a Next.js app — that is
|
|
50
|
+
four minutes and a dozen steps the starter already did.
|
|
51
|
+
|
|
52
|
+
### Other stacks
|
|
35
53
|
|
|
36
54
|
Nothing you run has a keyboard. A scaffolder that asks "Would you like to use
|
|
37
55
|
TypeScript?" gets no answer and fails, so give it every answer up front:
|
package/skills/ui-ux/SKILL.md
CHANGED
|
@@ -286,8 +286,8 @@ because it was first to hand means you were not deciding.
|
|
|
286
286
|
Run it (`npm run dev` starts in the background and returns the URL), then call
|
|
287
287
|
`look_at_app` with that URL. It opens the app in a real browser at 375px and
|
|
288
288
|
1440px, reports console errors, failed requests, overflow, broken images and
|
|
289
|
-
unlabeled controls, and returns a designer's review of the
|
|
290
|
-
it finds and look
|
|
289
|
+
unlabeled controls, and the first look returns a designer's review of the
|
|
290
|
+
screenshots. Fix what it finds in one pass and look once more. Then check:
|
|
291
291
|
|
|
292
292
|
1. **375px wide** — no horizontal scroll, nothing overlapping or clipped. If you
|
|
293
293
|
wrote no responsive rules at all, you have not done this.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* doctor.js — `ucode doctor` and /doctor: is everything ucode needs working?
|
|
3
|
+
*
|
|
4
|
+
* Every check runs at once and each has a short deadline, so the whole report
|
|
5
|
+
* takes a few seconds. A problem comes with the one thing that fixes it.
|
|
6
|
+
* Secret values are never printed — only whether they are there and accepted.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { theme, dim, blue } from '../ui/theme.js';
|
|
14
|
+
import { VERSION } from './version.js';
|
|
15
|
+
import { DEFAULT_MODEL, ENV_FILE } from './provider.js';
|
|
16
|
+
import { newer } from './updater.js';
|
|
17
|
+
|
|
18
|
+
const DEADLINE = 6000;
|
|
19
|
+
const timed = (ms = DEADLINE) => AbortSignal.timeout(ms);
|
|
20
|
+
|
|
21
|
+
function version(cmd) {
|
|
22
|
+
const r = spawnSync(cmd, ['--version'], { encoding: 'utf8', shell: process.platform === 'win32', timeout: 5000 });
|
|
23
|
+
return r.status === 0 ? String(r.stdout).trim().split('\n')[0] : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function checkKey() {
|
|
27
|
+
const key = process.env.UCODE_API_KEY || process.env.OPENROUTER_API_KEY;
|
|
28
|
+
if (!key) return { ok: false, name: 'API key', detail: 'not set', fix: `Add UCODE_API_KEY=... to ${ENV_FILE}` };
|
|
29
|
+
try {
|
|
30
|
+
const r = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
|
|
33
|
+
body: JSON.stringify({ model: DEFAULT_MODEL, messages: [{ role: 'user', content: 'ok' }], max_tokens: 1 }),
|
|
34
|
+
signal: timed(),
|
|
35
|
+
});
|
|
36
|
+
if (r.status === 401 || r.status === 403) return { ok: false, name: 'API key', detail: 'rejected', fix: 'Check the key for typos, or make a new one' };
|
|
37
|
+
const body = await r.json().catch(() => ({}));
|
|
38
|
+
const meta = body?.error?.metadata?.headers ?? {};
|
|
39
|
+
const left = r.headers.get('x-ratelimit-remaining') ?? meta['X-RateLimit-Remaining'];
|
|
40
|
+
const reset = Number(r.headers.get('x-ratelimit-reset') ?? meta['X-RateLimit-Reset']);
|
|
41
|
+
if (r.status === 429 && /per[- ]day|daily/i.test(`${body?.error?.message} ${body?.error?.metadata?.limit_source}`)) {
|
|
42
|
+
const at = Number.isFinite(reset) ? new Date(reset).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : 'tomorrow';
|
|
43
|
+
return { ok: false, name: 'Requests today', detail: 'daily free limit used up', fix: `It resets at ${at}; adding credit raises the limit` };
|
|
44
|
+
}
|
|
45
|
+
return { ok: true, name: 'API key', detail: left != null ? `works · ${left} free requests left today` : 'works' };
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return { ok: false, name: 'API key', detail: `could not check (${err.name === 'TimeoutError' ? 'no answer' : err.message})`, fix: 'Check your internet connection' };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function checkVercel() {
|
|
52
|
+
const token = process.env.VERCEL_TOKEN;
|
|
53
|
+
if (!token) return { ok: null, name: 'Vercel', detail: 'no token — /deploy needs one', fix: `vercel.com/account/tokens → Create Token, then add VERCEL_TOKEN=... to ${ENV_FILE}` };
|
|
54
|
+
try {
|
|
55
|
+
const r = await fetch('https://api.vercel.com/v2/user', { headers: { Authorization: `Bearer ${token}` }, signal: timed() });
|
|
56
|
+
if (!r.ok) return { ok: false, name: 'Vercel', detail: 'token rejected', fix: 'Make a new token at vercel.com/account/tokens' };
|
|
57
|
+
const j = await r.json();
|
|
58
|
+
return { ok: true, name: 'Vercel', detail: `ready to deploy as ${j.user?.username ?? 'you'}` };
|
|
59
|
+
} catch {
|
|
60
|
+
return { ok: false, name: 'Vercel', detail: 'could not reach Vercel', fix: 'Check your internet connection' };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function checkBrowser() {
|
|
65
|
+
try {
|
|
66
|
+
const { chromium } = await import('playwright-core');
|
|
67
|
+
for (const channel of ['msedge', 'chrome']) {
|
|
68
|
+
try {
|
|
69
|
+
const b = await chromium.launch({ channel, headless: true, timeout: 8000 });
|
|
70
|
+
await b.close();
|
|
71
|
+
return { ok: true, name: 'Browser', detail: `${channel === 'msedge' ? 'Edge' : 'Chrome'} — look_at_app can see your apps` };
|
|
72
|
+
} catch { /* try the next one */ }
|
|
73
|
+
}
|
|
74
|
+
} catch { /* playwright-core missing */ }
|
|
75
|
+
return { ok: false, name: 'Browser', detail: 'no Edge or Chrome found', fix: 'Install Google Chrome or Microsoft Edge' };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function checkVersion() {
|
|
79
|
+
try {
|
|
80
|
+
const r = await fetch('https://registry.npmjs.org/ucode-agent/latest', { signal: timed() });
|
|
81
|
+
const latest = (await r.json()).version;
|
|
82
|
+
return newer(latest, VERSION)
|
|
83
|
+
? { ok: null, name: 'ucode', detail: `${VERSION} · ${latest} is out`, fix: 'It updates itself on the next start, or: npm install -g ucode-agent' }
|
|
84
|
+
: { ok: true, name: 'ucode', detail: `${VERSION} · up to date` };
|
|
85
|
+
} catch {
|
|
86
|
+
return { ok: true, name: 'ucode', detail: VERSION };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function checkTools() {
|
|
91
|
+
const [major] = process.versions.node.split('.').map(Number);
|
|
92
|
+
const out = [
|
|
93
|
+
major >= 22
|
|
94
|
+
? { ok: true, name: 'Node', detail: process.versions.node }
|
|
95
|
+
: { ok: false, name: 'Node', detail: process.versions.node, fix: 'Install Node 22 or newer from nodejs.org' },
|
|
96
|
+
];
|
|
97
|
+
const npm = version('npm');
|
|
98
|
+
out.push(npm ? { ok: true, name: 'npm', detail: npm } : { ok: false, name: 'npm', detail: 'not found', fix: 'Reinstall Node, which includes npm' });
|
|
99
|
+
const git = version('git');
|
|
100
|
+
out.push(git ? { ok: true, name: 'git', detail: git.replace(/^git version /, '') } : { ok: null, name: 'git', detail: 'not found', fix: 'Optional — install git to keep history of your apps' });
|
|
101
|
+
const cache = path.join(os.homedir(), '.ucode', 'cache');
|
|
102
|
+
if (existsSync(cache)) out.push({ ok: true, name: 'Starter cache', detail: `${readdirSync(cache).length} ready` });
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Run every check; returns the lines to print. */
|
|
107
|
+
export async function runDoctor() {
|
|
108
|
+
const results = [
|
|
109
|
+
...checkTools(),
|
|
110
|
+
...(await Promise.all([checkKey(), checkVercel(), checkBrowser(), checkVersion()])),
|
|
111
|
+
];
|
|
112
|
+
const width = Math.max(...results.map((r) => r.name.length));
|
|
113
|
+
const lines = ['', ` ${blue('ucode doctor')}`];
|
|
114
|
+
for (const r of results) {
|
|
115
|
+
const mark = r.ok === true ? theme.ok('✓') : r.ok === false ? theme.error('✗') : theme.warn('!');
|
|
116
|
+
lines.push(` ${mark} ${r.name.padEnd(width)} ${r.detail}`);
|
|
117
|
+
if (r.fix && r.ok !== true) lines.push(` ${' '.repeat(width)} ${dim(`→ ${r.fix}`)}`);
|
|
118
|
+
}
|
|
119
|
+
const bad = results.filter((r) => r.ok === false).length;
|
|
120
|
+
lines.push('', bad ? ` ${bad} thing${bad === 1 ? '' : 's'} to fix` : ` ${theme.ok('All good')}`, '');
|
|
121
|
+
return lines;
|
|
122
|
+
}
|