ucode-agent 1.62.3 → 1.62.5
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 +1 -1
- package/package.json +63 -63
- package/skills/build-app/DIGEST.md +154 -145
- package/skills/build-app/SKILL.md +10 -1
- package/src/core/loop.js +89 -24
- package/src/core/opener.js +44 -0
- package/src/core/scope.js +23 -0
- package/src/tools/browser.js +58 -0
- package/src/tools/index.js +5 -1
- package/src/tools/shell.js +15 -0
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,63 +1,63 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "ucode-agent",
|
|
3
|
-
"version": "1.62.
|
|
4
|
-
"description": "ucode - a terminal coding agent that reads, edits and runs your code, on Google Gemini models.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "ucode.js",
|
|
7
|
-
"bin": {
|
|
8
|
-
"ucode": "ucode.js"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"ucode.js",
|
|
12
|
-
"src/",
|
|
13
|
-
"skills/",
|
|
14
|
-
"templates/",
|
|
15
|
-
"THIRD_PARTY_NOTICES.md"
|
|
16
|
-
],
|
|
17
|
-
"scripts": {
|
|
18
|
-
"start": "node ucode.js",
|
|
19
|
-
"test": "node test/run.js",
|
|
20
|
-
"prepublishOnly": "node scripts/no-bundled-key.js && node test/run.js",
|
|
21
|
-
"hooks": "node scripts/install-hooks.js"
|
|
22
|
-
},
|
|
23
|
-
"repository": {
|
|
24
|
-
"type": "git",
|
|
25
|
-
"url": "git+https://github.com/sppideey/ucode-agent.git"
|
|
26
|
-
},
|
|
27
|
-
"bugs": {
|
|
28
|
-
"url": "https://github.com/sppideey/ucode-agent/issues"
|
|
29
|
-
},
|
|
30
|
-
"homepage": "https://github.com/sppideey/ucode-agent#readme",
|
|
31
|
-
"engines": {
|
|
32
|
-
"node": ">=22"
|
|
33
|
-
},
|
|
34
|
-
"keywords": [
|
|
35
|
-
"agent",
|
|
36
|
-
"cli",
|
|
37
|
-
"terminal",
|
|
38
|
-
"coding-agent",
|
|
39
|
-
"llm",
|
|
40
|
-
"gemini",
|
|
41
|
-
"google-gemini",
|
|
42
|
-
"ai"
|
|
43
|
-
],
|
|
44
|
-
"author": "om dixit",
|
|
45
|
-
"license": "MIT",
|
|
46
|
-
"dependencies": {
|
|
47
|
-
"@babel/parser": "^7.29.9",
|
|
48
|
-
"chalk": "^6.0.0",
|
|
49
|
-
"dotenv": "^18.0.3",
|
|
50
|
-
"jsonrepair": "^3.15.0",
|
|
51
|
-
"marked": "^15.0.12",
|
|
52
|
-
"marked-terminal": "^7.3.0",
|
|
53
|
-
"openai": "^7.23.0",
|
|
54
|
-
"playwright-core": "^1.63.0"
|
|
55
|
-
},
|
|
56
|
-
"devDependencies": {
|
|
57
|
-
"@types/node": "^26.6.2",
|
|
58
|
-
"typescript": "^5.9.3"
|
|
59
|
-
},
|
|
60
|
-
"publishConfig": {
|
|
61
|
-
"access": "public"
|
|
62
|
-
}
|
|
63
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "ucode-agent",
|
|
3
|
+
"version": "1.62.5",
|
|
4
|
+
"description": "ucode - a terminal coding agent that reads, edits and runs your code, on Google Gemini models.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "ucode.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ucode": "ucode.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"ucode.js",
|
|
12
|
+
"src/",
|
|
13
|
+
"skills/",
|
|
14
|
+
"templates/",
|
|
15
|
+
"THIRD_PARTY_NOTICES.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"start": "node ucode.js",
|
|
19
|
+
"test": "node test/run.js",
|
|
20
|
+
"prepublishOnly": "node scripts/no-bundled-key.js && node test/run.js",
|
|
21
|
+
"hooks": "node scripts/install-hooks.js"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/sppideey/ucode-agent.git"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/sppideey/ucode-agent/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/sppideey/ucode-agent#readme",
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=22"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"agent",
|
|
36
|
+
"cli",
|
|
37
|
+
"terminal",
|
|
38
|
+
"coding-agent",
|
|
39
|
+
"llm",
|
|
40
|
+
"gemini",
|
|
41
|
+
"google-gemini",
|
|
42
|
+
"ai"
|
|
43
|
+
],
|
|
44
|
+
"author": "om dixit",
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@babel/parser": "^7.29.9",
|
|
48
|
+
"chalk": "^6.0.0",
|
|
49
|
+
"dotenv": "^18.0.3",
|
|
50
|
+
"jsonrepair": "^3.15.0",
|
|
51
|
+
"marked": "^15.0.12",
|
|
52
|
+
"marked-terminal": "^7.3.0",
|
|
53
|
+
"openai": "^7.23.0",
|
|
54
|
+
"playwright-core": "^1.63.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/node": "^26.6.2",
|
|
58
|
+
"typescript": "^5.9.3"
|
|
59
|
+
},
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -1,145 +1,154 @@
|
|
|
1
|
-
# Building something from nothing — the short form
|
|
2
|
-
|
|
3
|
-
The failure mode is not bad code. It is a folder of files that has never been
|
|
4
|
-
run, handed over as if it works.
|
|
5
|
-
|
|
6
|
-
## 1. Decide the shape before any file exists
|
|
7
|
-
|
|
8
|
-
One line each: **what it does**, **the core loop** (the one path that must work
|
|
9
|
-
perfectly), **the stack**, **the file list**.
|
|
10
|
-
|
|
11
|
-
| Need | Choose |
|
|
12
|
-
| --- | --- |
|
|
13
|
-
| One page, no secrets, no server | `create_app` with `plain-html` |
|
|
14
|
-
| Interactive client app, no secrets | `plain-html` still, unless it truly needs a build |
|
|
15
|
-
| Pages plus a server, secrets, API routes, SEO | `create_app` with `next-shadcn` |
|
|
16
|
-
| An API on its own | Node (Hono/Express) or Python (FastAPI) |
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
1
|
+
# Building something from nothing — the short form
|
|
2
|
+
|
|
3
|
+
The failure mode is not bad code. It is a folder of files that has never been
|
|
4
|
+
run, handed over as if it works.
|
|
5
|
+
|
|
6
|
+
## 1. Decide the shape before any file exists
|
|
7
|
+
|
|
8
|
+
One line each: **what it does**, **the core loop** (the one path that must work
|
|
9
|
+
perfectly), **the stack**, **the file list**.
|
|
10
|
+
|
|
11
|
+
| Need | Choose |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| One page, no secrets, no server | `create_app` with `plain-html` |
|
|
14
|
+
| Interactive client app, no secrets | `plain-html` still, unless it truly needs a build |
|
|
15
|
+
| Pages plus a server, secrets, API routes, SEO | `create_app` with `next-shadcn` |
|
|
16
|
+
| An API on its own | Node (Hono/Express) or Python (FastAPI) |
|
|
17
|
+
|
|
18
|
+
**Scope: complete, not sprawling.** Build what was asked, and build all of it
|
|
19
|
+
well: every feature someone using that kind of app expects on first use — a
|
|
20
|
+
tasks app adds, edits, ticks off, deletes, filters and remembers across a
|
|
21
|
+
reload; a quiz has questions, scoring, feedback and a restart; a game has
|
|
22
|
+
rules, a score, win and lose, and play again. Nothing from a different app: no
|
|
23
|
+
timer, calendar or analytics dashboard bolted onto a tasks app, no accounts on
|
|
24
|
+
a quiz. Features nobody named cost minutes and are where builds break — if one
|
|
25
|
+
is worth it, offer it in one line at the end instead of building it.
|
|
26
|
+
|
|
27
|
+
Pick the smallest stack that does the job and mean it: a tasks app, a
|
|
28
|
+
calculator, a timer, a game, a visualisation — all one page. Next.js costs an
|
|
29
|
+
install and a build, minutes the user waits through, and buys nothing an app
|
|
30
|
+
with no server needs.
|
|
31
|
+
|
|
32
|
+
## 2. Start from the starter — and finish in the same call
|
|
33
|
+
|
|
34
|
+
`create_app` takes `files`, so for a one-page app the scaffold and the whole
|
|
35
|
+
app are one call:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
create_app({ folder: "tide", name: "Tide", files: [
|
|
39
|
+
{ path: "tide/index.html", content: "…" },
|
|
40
|
+
{ path: "tide/styles.css", content: "…" },
|
|
41
|
+
{ path: "tide/app.js", content: "…" },
|
|
42
|
+
]})
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Every round trip is ten to forty seconds of the user's time, so one call
|
|
46
|
+
instead of four is most of how long the build takes.
|
|
47
|
+
|
|
48
|
+
- `plain-html` is the default: three files, no install, no build. Its files
|
|
49
|
+
come back in full inside the result — **never read them back**.
|
|
50
|
+
- `next-shadcn` installs in the background; commands in that folder wait for
|
|
51
|
+
it on their own, so start writing components at once. Re-tint `globals.css`
|
|
52
|
+
for the app's direction rather than shipping the slate default.
|
|
53
|
+
- Never run `create-next-app` or `shadcn init`. Nothing you run has a
|
|
54
|
+
keyboard: every scaffolder needs its answers as flags up front.
|
|
55
|
+
|
|
56
|
+
## 2a. Paths in `files` are not the paths in the page
|
|
57
|
+
|
|
58
|
+
The two are relative to different things, and getting them confused is the
|
|
59
|
+
commonest way a finished build comes up as bare markup. `files` paths are
|
|
60
|
+
relative to the **project root**, so they carry the app folder. A link inside a
|
|
61
|
+
page is relative to **that page**, so it must not.
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
create_app({ folder: "tide", name: "Tide", files: [
|
|
65
|
+
{ path: "tide/index.html", content: "... <link rel=stylesheet href=\"styles.css\">
|
|
66
|
+
<script type=module src=\"app.js\"></script> ..." },
|
|
67
|
+
{ path: "tide/styles.css", content: "..." },
|
|
68
|
+
{ path: "tide/app.js", content: "..." },
|
|
69
|
+
]})
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Wrong, and it 404s: `href="tide/styles.css"` inside `tide/index.html` — the
|
|
73
|
+
browser resolves that to `tide/tide/styles.css`, so no stylesheet and no script
|
|
74
|
+
load, and the page is unstyled markup with dead buttons.
|
|
75
|
+
|
|
76
|
+
## 2b. Look before you create
|
|
77
|
+
|
|
78
|
+
`create_app` refuses a folder that already has something in it unless you pass
|
|
79
|
+
the app in `files` — and a folder you half-made on an earlier attempt counts.
|
|
80
|
+
One `list_dir` in front of it costs a second and tells you whether you are
|
|
81
|
+
starting or resuming. The same goes for any command that makes a directory.
|
|
82
|
+
|
|
83
|
+
## 2c. Do not type what already exists
|
|
84
|
+
|
|
85
|
+
`add_block` has the pieces every app needs, written for whichever starter this
|
|
86
|
+
one uses: a list you can add to, tick off, rename and remove; a filter row; a
|
|
87
|
+
localStorage store; a dialog; toasts; a theme toggle; a table; an empty state.
|
|
88
|
+
Call it before writing any of those by hand. Each is a hundred lines you skip,
|
|
89
|
+
and typing is the slowest part of a build — a page assembled from blocks is
|
|
90
|
+
done minutes before the same page typed out. Call `add_block` with no name to
|
|
91
|
+
see what fits this app.
|
|
92
|
+
|
|
93
|
+
## 3. Structure
|
|
94
|
+
|
|
95
|
+
One component per file, named for what it is, not a 600-line `page.tsx`. In
|
|
96
|
+
Next.js: `src/app` (routes, `globals.css`, `api/<name>/route.ts`),
|
|
97
|
+
`src/components/<feature>/`, `src/lib/` for outside services and schemas.
|
|
98
|
+
Server components by default, `"use client"` only where it is interactive.
|
|
99
|
+
Types at every boundary; parse external data rather than trusting its shape.
|
|
100
|
+
|
|
101
|
+
## 4. Secrets and outside services
|
|
102
|
+
|
|
103
|
+
- **A key never reaches the browser.** It lives in a server route. Anything
|
|
104
|
+
imported by a `"use client"` file ships to every visitor, including a
|
|
105
|
+
"hardcoded for now" key — put it in a server-only module and say where.
|
|
106
|
+
- Every outbound call gets a timeout (`AbortSignal.timeout(60_000)`), a status
|
|
107
|
+
check, and an error that says what failed — surfaced as a real message,
|
|
108
|
+
never a silent `catch {}`.
|
|
109
|
+
- Calling a model: ask for JSON and parse it defensively (extract the first
|
|
110
|
+
`{...}`, validate, clamp numbers), put the judgement rules in the prompt
|
|
111
|
+
explicitly, and make the route timeout longer than the model takes.
|
|
112
|
+
|
|
113
|
+
## 5. Build order
|
|
114
|
+
|
|
115
|
+
Skeleton and design tokens first, so everything after is styled correctly the
|
|
116
|
+
first time; then the server route with the real integration; then the core
|
|
117
|
+
loop UI wired to it; then every state — empty, loading, success, error,
|
|
118
|
+
invalid input; then polish: motion, responsive, copy, title and metadata.
|
|
119
|
+
|
|
120
|
+
## 5a. It has to look finished
|
|
121
|
+
|
|
122
|
+
People judge the app in the first second it is on screen, before they click
|
|
123
|
+
anything. Before handing it over, check:
|
|
124
|
+
|
|
125
|
+
- A real title and one line saying what it is for — never "Welcome to…".
|
|
126
|
+
- The main action is visible without scrolling and is the most prominent button.
|
|
127
|
+
- Every button and input has hover and focus states; nothing is left browser-default.
|
|
128
|
+
- An empty list says what to do next ("No tasks yet — add one above"), never blank.
|
|
129
|
+
- It fits a 375px phone with no sideways scroll; tap targets are at least 44px.
|
|
130
|
+
- The number that matters (a score, a total, a timer) is big — several times body text.
|
|
131
|
+
|
|
132
|
+
## 5b. Tests, where there is a runner
|
|
133
|
+
|
|
134
|
+
`next-shadcn` ships vitest and one passing test, so `npm test` works from
|
|
135
|
+
the first minute — add cases for the core loop as you build it, not after, and
|
|
136
|
+
ucode will run the ones that touch whatever you change. Assert behaviour: that
|
|
137
|
+
adding an item puts it in the list, that the total is right, that an empty
|
|
138
|
+
input is refused.
|
|
139
|
+
|
|
140
|
+
`plain-html` has no runner and installs nothing, by design. Its test is the
|
|
141
|
+
browser check: ucode opens the app, types into the first field, presses Enter
|
|
142
|
+
and clicks the button that submits. Make sure that path is the one that works.
|
|
143
|
+
|
|
144
|
+
## 6. Prove it works, then report
|
|
145
|
+
|
|
146
|
+
`npm run build` type-checks and lints — a build that fails is not done. Start
|
|
147
|
+
it (`npm run dev` backgrounds itself and returns the URL; do not start it
|
|
148
|
+
twice), then `look_at_app` on every page. A clean build proves it compiles,
|
|
149
|
+
not that it works. Fix what you find and check again.
|
|
150
|
+
|
|
151
|
+
Done means: the core loop works end to end, no TODO, no placeholder copy, no
|
|
152
|
+
dead buttons, no console errors, every async action has its states, secrets
|
|
153
|
+
server-side, build passes. Then say what you built, how to run it, and — in
|
|
154
|
+
one sentence — anything you did not finish or could not test.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: build-app
|
|
3
3
|
description: Take an app from nothing to running and finished — stack choice, non-interactive scaffolding, project structure, secrets, AI and API integration, error handling, and proving it works before saying it does.
|
|
4
|
-
auto: scaffold, new project, from scratch, build an app, make an app, create an app, build a website, make a website, build a site, build me a, next.js app, nextjs app, next app, react app, vite app, shadcn, create-next-app, full stack, fullstack, saas, mvp
|
|
4
|
+
auto: scaffold, new project, from scratch, build an app, make an app, create an app, make me a, make me an, build me an, create me a, build a website, make a website, build a site, build me a, next.js app, nextjs app, next app, react app, vite app, shadcn, create-next-app, full stack, fullstack, saas, mvp
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Building something from nothing
|
|
@@ -32,6 +32,15 @@ One line each:
|
|
|
32
32
|
|
|
33
33
|
- **The file list** — the whole tree, before creating any of it.
|
|
34
34
|
|
|
35
|
+
**Scope: complete, not sprawling.** Build what was asked, and build all of it
|
|
36
|
+
well: every feature someone using that kind of app expects on first use — a
|
|
37
|
+
tasks app adds, edits, ticks off, deletes, filters and remembers across a
|
|
38
|
+
reload; a quiz has questions, scoring, feedback and a restart; a game has
|
|
39
|
+
rules, a score, win and lose, and play again. Nothing from a different app: no
|
|
40
|
+
timer, calendar or analytics dashboard bolted onto a tasks app, no accounts on
|
|
41
|
+
a quiz. Features nobody named cost minutes and are where builds break — if one
|
|
42
|
+
is worth it, offer it in one line at the end instead of building it.
|
|
43
|
+
|
|
35
44
|
If there is a user interface, the `ui-ux` skill is already loaded. Decide the
|
|
36
45
|
design direction now, not after the logic works. If the app calls a model,
|
|
37
46
|
load `ai-features`; if it has accounts, keys or uploads, load `security`.
|
package/src/core/loop.js
CHANGED
|
@@ -50,6 +50,9 @@ import { Failure, ToolFailure, Declined } from './failure.js';
|
|
|
50
50
|
import { StuckWatch, eventFor, describeHit } from './stuck.js';
|
|
51
51
|
import { serversReadySince } from '../tools/shell.js';
|
|
52
52
|
import { formatDuration } from '../ui/activity.js';
|
|
53
|
+
import { MAX_FILE_OUTPUT } from '../tools/shared.js';
|
|
54
|
+
import { openInBrowser } from './opener.js';
|
|
55
|
+
import { withScope } from './scope.js';
|
|
53
56
|
import { runDoctor } from './doctor.js';
|
|
54
57
|
import { JS_LOGIC } from './jslogic.js';
|
|
55
58
|
import { deploy } from '../tools/deploy.js';
|
|
@@ -109,6 +112,12 @@ const SILENT = new Set(['update_plan']);
|
|
|
109
112
|
/** How many rounds of "the type check found errors, fix them" one turn may take. */
|
|
110
113
|
const MAX_FIX_ROUNDS = 3;
|
|
111
114
|
|
|
115
|
+
/** A file up to this many lines is sent whole on its first read, whatever slice was asked for. */
|
|
116
|
+
const WHOLE_READ_LINES = 1500;
|
|
117
|
+
|
|
118
|
+
/** Lookups in a row, during a fix round with nothing changed, before ucode says to stop reading. */
|
|
119
|
+
const LOOKUP_NUDGE = 5;
|
|
120
|
+
|
|
112
121
|
/** Files worth checking after they change. */
|
|
113
122
|
/** A file with a page in it — something a browser can be pointed at. */
|
|
114
123
|
const PAGE = /\.(?:html?|tsx|jsx)$/i;
|
|
@@ -621,6 +630,15 @@ function systemPrompt({ cwd, skills, mode, check, map, memory }) {
|
|
|
621
630
|
'already contains it. Do not re-check work the checks have already reported on.',
|
|
622
631
|
'Fast is not sloppy: it is the same work with the waiting taken out.',
|
|
623
632
|
'',
|
|
633
|
+
'BUILD WHAT WAS ASKED - ALL OF IT, AND NOTHING ELSE. A short request ("a tasks',
|
|
634
|
+
'app", "a quiz", "a snake game") still gets a complete, well-made app: every',
|
|
635
|
+
'feature someone using that kind of app expects on first use. A tasks app adds,',
|
|
636
|
+
'edits, ticks off, deletes, filters and remembers across a reload. It does NOT',
|
|
637
|
+
'also get a pomodoro timer, a calendar, an analytics dashboard, stats charts or a',
|
|
638
|
+
'shortcuts panel - those are other apps. Extra views are where builds break, and',
|
|
639
|
+
'each one costs the user minutes. One screen that does its job perfectly beats',
|
|
640
|
+
'four that half work. An extra worth having: offer it in one line at the end.',
|
|
641
|
+
'',
|
|
624
642
|
'DESIGN IT BEFORE YOU TYPE IT. Fast means fewer round trips. It does not mean a',
|
|
625
643
|
'default theme, and an app that goes out in the palette its starter came with is',
|
|
626
644
|
'not a fast build, it is an undesigned one. There is no design pass after the',
|
|
@@ -632,7 +650,7 @@ function systemPrompt({ cwd, skills, mode, check, map, memory }) {
|
|
|
632
650
|
' - An ACCENT that is not the one the starter shipped with.',
|
|
633
651
|
' - ONE memorable thing this app has that other apps do not: a colour, a type',
|
|
634
652
|
' move, a texture, one interaction. Exactly one. It is the difference between',
|
|
635
|
-
' a design and a theme.',
|
|
653
|
+
' a design and a theme - a design choice, never an extra feature or view.',
|
|
636
654
|
'Name the tone and the accent in your opening line, so they are settled before any',
|
|
637
655
|
'file exists: "I will build Tide - a tasks app in one HTML file, calm, warm grey',
|
|
638
656
|
'with a single amber accent." That is not narrating a plan, that is the decision.',
|
|
@@ -1205,6 +1223,9 @@ export class Agent {
|
|
|
1205
1223
|
this.appName = null;
|
|
1206
1224
|
this.appTemplate = null;
|
|
1207
1225
|
this.reads = new Map();
|
|
1226
|
+
this.fixing = false;
|
|
1227
|
+
this.lookups = 0;
|
|
1228
|
+
this.nudgedAt = 0;
|
|
1208
1229
|
this.declines = 0;
|
|
1209
1230
|
this.apps = [];
|
|
1210
1231
|
this.wrote = new Map();
|
|
@@ -1216,9 +1237,8 @@ export class Agent {
|
|
|
1216
1237
|
// model answered: asked for a dark mode toggle on its second turn, a live
|
|
1217
1238
|
// run re-read two files and repeated its first turn's summary instead.
|
|
1218
1239
|
this.autoLoad(input);
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
: { role: 'user', content: input });
|
|
1240
|
+
const request = images.length ? { role: 'user', content: input, images } : { role: 'user', content: input };
|
|
1241
|
+
this.push(request);
|
|
1222
1242
|
|
|
1223
1243
|
if (!this.session.title || this.session.title === 'Untitled') {
|
|
1224
1244
|
this.session.title = titleFrom(input);
|
|
@@ -1231,6 +1251,7 @@ export class Agent {
|
|
|
1231
1251
|
// Nothing to look up in an empty folder, so those tools do not go out with
|
|
1232
1252
|
// the request. Decided per turn: the moment there is code, they are back.
|
|
1233
1253
|
this.fresh = !hasCode(this.map);
|
|
1254
|
+
request.content = withScope(input, { fresh: this.fresh });
|
|
1234
1255
|
this.wantsWeb = WANTS_WEB.test(input);
|
|
1235
1256
|
await this.persist();
|
|
1236
1257
|
|
|
@@ -1486,6 +1507,7 @@ export class Agent {
|
|
|
1486
1507
|
const problems = await this.autoCheck();
|
|
1487
1508
|
if (problems) {
|
|
1488
1509
|
fixRounds++;
|
|
1510
|
+
this.fixing = true;
|
|
1489
1511
|
if (reply.text) this.push({ role: 'assistant', content: reply.text });
|
|
1490
1512
|
this.push({
|
|
1491
1513
|
role: 'user',
|
|
@@ -1571,6 +1593,7 @@ export class Agent {
|
|
|
1571
1593
|
if (handed?.done) return;
|
|
1572
1594
|
if (handed?.problems && fixRounds < MAX_FIX_ROUNDS) {
|
|
1573
1595
|
fixRounds++;
|
|
1596
|
+
this.fixing = true;
|
|
1574
1597
|
this.push({
|
|
1575
1598
|
role: 'user',
|
|
1576
1599
|
content:
|
|
@@ -1643,8 +1666,16 @@ export class Agent {
|
|
|
1643
1666
|
const noted = (call) => {
|
|
1644
1667
|
if (FILE_WRITES.has(call.name)) {
|
|
1645
1668
|
for (const p of pathsOf(call)) { this.touched.add(p); this.sinceCheck.add(p); this.forgetReads(p); }
|
|
1669
|
+
this.lookups = 0;
|
|
1670
|
+
this.nudgedAt = 0;
|
|
1671
|
+
} else if (PARALLEL_SAFE.has(call.name)) {
|
|
1672
|
+
this.lookups = (this.lookups ?? 0) + 1;
|
|
1673
|
+
}
|
|
1674
|
+
if (call.name === 'run_command' || call.name === 'run_commands') {
|
|
1675
|
+
this.ranSomething = true;
|
|
1676
|
+
// A command can rewrite any file, so no whole read is still known to be current.
|
|
1677
|
+
for (const key of this.reads?.keys() ?? []) if (key.endsWith('#whole')) this.reads.delete(key);
|
|
1646
1678
|
}
|
|
1647
|
-
if (call.name === 'run_command' || call.name === 'run_commands') this.ranSomething = true;
|
|
1648
1679
|
};
|
|
1649
1680
|
|
|
1650
1681
|
if (group.length > 1) {
|
|
@@ -1753,26 +1784,10 @@ export class Agent {
|
|
|
1753
1784
|
if (this.openInBrowser(server.url)) this.ui.note(`Opened ${server.url} in your browser`);
|
|
1754
1785
|
}
|
|
1755
1786
|
|
|
1756
|
-
/**
|
|
1757
|
-
* Show a URL or a local page in the desktop browser. False when it did not.
|
|
1758
|
-
*
|
|
1759
|
-
* A file goes to explorer on Windows, not `cmd /c start`: the folder name
|
|
1760
|
-
* comes from the model, and an & in it would be a second command to cmd.
|
|
1761
|
-
*/
|
|
1787
|
+
/** Show a URL or a page in the project in the desktop browser (see opener.js). UCODE_OPEN=0 turns it off. */
|
|
1762
1788
|
openInBrowser(target) {
|
|
1763
1789
|
if (!this.full || process.env.UCODE_OPEN === '0') return false;
|
|
1764
|
-
|
|
1765
|
-
// Only pages inside the project: a UNC path would have explorer reach out to another machine.
|
|
1766
|
-
const rel = web ? '' : path.relative(this.cwd, target);
|
|
1767
|
-
if (!web && (!rel || rel.startsWith('..') || path.isAbsolute(rel))) return false;
|
|
1768
|
-
const [cmd, args] = process.platform === 'win32'
|
|
1769
|
-
? (web ? ['cmd', ['/c', 'start', '', target]] : ['explorer.exe', [target]])
|
|
1770
|
-
: [process.platform === 'darwin' ? 'open' : 'xdg-open', [target]];
|
|
1771
|
-
try {
|
|
1772
|
-
// A missing opener (no xdg-open) fails later, as an event; unheard, it would crash ucode.
|
|
1773
|
-
spawn(cmd, args, { stdio: 'ignore', detached: true, windowsHide: true }).on('error', () => {}).unref();
|
|
1774
|
-
return true;
|
|
1775
|
-
} catch { return false; /* no browser to open — the link is in the answer */ }
|
|
1790
|
+
return openInBrowser(target, { root: this.cwd });
|
|
1776
1791
|
}
|
|
1777
1792
|
|
|
1778
1793
|
cmdStats() {
|
|
@@ -1878,7 +1893,34 @@ export class Agent {
|
|
|
1878
1893
|
const found = /^(\d+) problem/.exec(out.summary ?? '');
|
|
1879
1894
|
this.ui.runStat?.(found ? `${found[1]} to fix` : 'clean');
|
|
1880
1895
|
}
|
|
1881
|
-
this.push({ role: 'tool', toolCallId: call.id, name: call.name, content: out.content + this.stuckNote(call, { out }) });
|
|
1896
|
+
this.push({ role: 'tool', toolCallId: call.id, name: call.name, content: out.content + this.stuckNote(call, { out }) + this.lookupNote(call) });
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
/**
|
|
1900
|
+
* Reading on and on while ucode's list of errors waits. A live fix round
|
|
1901
|
+
* read a 1000-line app.js in seventeen slices, changed nothing, and ran the
|
|
1902
|
+
* free key into its per-minute limit: every read resends the conversation.
|
|
1903
|
+
*/
|
|
1904
|
+
lookupNote(call) {
|
|
1905
|
+
if (!this.fixing || !PARALLEL_SAFE.has(call.name)) return '';
|
|
1906
|
+
if (this.lookups < LOOKUP_NUDGE || this.lookups - this.nudgedAt < LOOKUP_NUDGE) return '';
|
|
1907
|
+
this.nudgedAt = this.lookups;
|
|
1908
|
+
return `\n\nSTOP reading - that is ${this.lookups} lookups since ucode listed the errors, and nothing has been ` +
|
|
1909
|
+
'changed yet. What you need is already above. Make the fix now with edit_file or multi_edit; ucode ' +
|
|
1910
|
+
'checks the app again straight after.';
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
/** Lines in a file small enough to send whole in one read, or null. */
|
|
1914
|
+
async wholeSize(file) {
|
|
1915
|
+
try {
|
|
1916
|
+
const text = await readFile(path.resolve(this.cwd, file), 'utf8');
|
|
1917
|
+
if (text.includes('\0')) return null;
|
|
1918
|
+
const lines = text.split('\n').length;
|
|
1919
|
+
// The line-number gutter adds about eight characters a line to what read_file sends.
|
|
1920
|
+
return lines <= WHOLE_READ_LINES && text.length + lines * 8 <= MAX_FILE_OUTPUT ? { lines } : null;
|
|
1921
|
+
} catch {
|
|
1922
|
+
return null;
|
|
1923
|
+
}
|
|
1882
1924
|
}
|
|
1883
1925
|
|
|
1884
1926
|
/**
|
|
@@ -2034,6 +2076,27 @@ export class Agent {
|
|
|
2034
2076
|
// refused as "unchanged", and a live build spent sixty steps being told so.
|
|
2035
2077
|
if (call.name === 'read_file' && call.args?.path) {
|
|
2036
2078
|
const file = String(call.args.path);
|
|
2079
|
+
|
|
2080
|
+
// A file that fits is sent whole the first time, and not again until
|
|
2081
|
+
// something writes to it. Slices were the loop: a live fix round read one
|
|
2082
|
+
// app.js in seventeen of them, never edited, and hit the per-minute limit.
|
|
2083
|
+
// The lead only — a worker has its own conversation, without the lead's reads.
|
|
2084
|
+
if (offered === this.offering) {
|
|
2085
|
+
const whole = `${fileKey(file)}#whole`;
|
|
2086
|
+
if (this.reads.has(whole)) {
|
|
2087
|
+
return {
|
|
2088
|
+
content: `All of ${file} is already above: you read the whole file and nothing has written to it ` +
|
|
2089
|
+
'since. Use that text instead of reading it again. If something in it needs changing, change it ' +
|
|
2090
|
+
'now with edit_file or multi_edit.',
|
|
2091
|
+
summary: 'unchanged, already read in full',
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
// Marked before the await, so two reads of one file in a parallel batch send it once.
|
|
2095
|
+
this.reads.set(whole, 1);
|
|
2096
|
+
const size = await this.wholeSize(file);
|
|
2097
|
+
if (size) call.args = { ...call.args, offset: 1, limit: size.lines };
|
|
2098
|
+
else this.reads.delete(whole);
|
|
2099
|
+
}
|
|
2037
2100
|
const key = `${fileKey(file)}#${Number(call.args.offset) || 1}:${Number(call.args.limit) || 0}`;
|
|
2038
2101
|
const seen = (this.reads ??= new Map()).get(key) ?? 0;
|
|
2039
2102
|
if (seen >= 2) {
|
|
@@ -2809,6 +2872,8 @@ export class Agent {
|
|
|
2809
2872
|
this.ui.stopSpinner();
|
|
2810
2873
|
if (result.folded) {
|
|
2811
2874
|
this.working = result.messages;
|
|
2875
|
+
// What was read is summarised away now, so it may be read again.
|
|
2876
|
+
this.reads = new Map();
|
|
2812
2877
|
this.ui.note(
|
|
2813
2878
|
`folded ${result.droppedCount} earlier messages into a summary ` +
|
|
2814
2879
|
'(the full history is still saved in this session)'
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opener.js — showing a finished page or a running server in the user's own
|
|
3
|
+
* browser.
|
|
4
|
+
*
|
|
5
|
+
* Kept apart from the loop because it is the one place ucode starts a program
|
|
6
|
+
* on a path the model chose, and that deserves to be read on its own.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
|
|
12
|
+
/** Is this a web address rather than a file on disk? */
|
|
13
|
+
const isWeb = (target) => /^https?:\/\//.test(target);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Only pages inside the project are opened. A folder name comes from the
|
|
17
|
+
* model, and a UNC path (\\host\share) would have explorer reach out to
|
|
18
|
+
* another machine.
|
|
19
|
+
*/
|
|
20
|
+
export function insideRoot(target, root) {
|
|
21
|
+
const rel = path.relative(root, target);
|
|
22
|
+
return Boolean(rel) && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Open a URL or a page inside `root`. Returns whether an opener was started.
|
|
27
|
+
*
|
|
28
|
+
* A file goes to explorer on Windows, not `cmd /c start`: an & in a folder
|
|
29
|
+
* name would be a second command to cmd.
|
|
30
|
+
*/
|
|
31
|
+
export function openInBrowser(target, { root }) {
|
|
32
|
+
const web = isWeb(target);
|
|
33
|
+
if (!web && !insideRoot(target, root)) return false;
|
|
34
|
+
const [cmd, args] = process.platform === 'win32'
|
|
35
|
+
? (web ? ['cmd', ['/c', 'start', '', target]] : ['explorer.exe', [target]])
|
|
36
|
+
: [process.platform === 'darwin' ? 'open' : 'xdg-open', [target]];
|
|
37
|
+
try {
|
|
38
|
+
// A missing opener (no xdg-open) fails later, as an event; unheard, it would crash ucode.
|
|
39
|
+
spawn(cmd, args, { stdio: 'ignore', detached: true, windowsHide: true }).on('error', () => {}).unref();
|
|
40
|
+
return true;
|
|
41
|
+
} catch {
|
|
42
|
+
return false; // no browser to open — the link is in the answer
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scope.js — keeping a new app to what was asked for.
|
|
3
|
+
*
|
|
4
|
+
* In the system prompt and the build-app skill alone, Flash-Lite still built
|
|
5
|
+
* "make me a tasks app" as a tasks app plus a pomodoro timer, a kanban board
|
|
6
|
+
* and analytics — twice running — and the extra views were where the builds
|
|
7
|
+
* broke. So the rule also rides on the request itself, the last thing the
|
|
8
|
+
* model reads before it starts.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const SCOPE_NOTE =
|
|
12
|
+
'(From ucode: if this asks for a new app, build it complete and well made, but as the one app asked ' +
|
|
13
|
+
'for - no extra views or tools such as timers, calendars, kanban boards, analytics, stats or an ' +
|
|
14
|
+
'editor for making your own, unless the request names them.)';
|
|
15
|
+
|
|
16
|
+
/** A request that may be for a new app. Loose on purpose: the note it adds says "if". */
|
|
17
|
+
export const BUILD_ASK =
|
|
18
|
+
/\b(?:make|build|create|design|code|write|generate|develop)\b|\bi\s+(?:want|need)\b|\b(?:app|game|website|site|page|tracker|calculator|quiz)\b/i;
|
|
19
|
+
|
|
20
|
+
/** The request as the model sees it: one in an empty folder, or shaped like a build, carries the note. */
|
|
21
|
+
export function withScope(input, { fresh = false } = {}) {
|
|
22
|
+
return fresh || BUILD_ASK.test(input) ? `${input}\n\n${SCOPE_NOTE}` : input;
|
|
23
|
+
}
|
package/src/tools/browser.js
CHANGED
|
@@ -232,6 +232,63 @@ const NOT_ADD = /search|filter|find|sort|query/i;
|
|
|
232
232
|
const LIST_HINT = /task|todo|to-do|item|note|add|new|entry|expense|habit|title|what needs|remind/i;
|
|
233
233
|
const ADD_BUTTON = /^\s*(?:\+|add|new|create|save|submit|post|done)\b|\b(?:add|create|save)\s/i;
|
|
234
234
|
|
|
235
|
+
/** A value a form will take for a field of this type, or null to leave the field alone. */
|
|
236
|
+
function sampleFor(type, today) {
|
|
237
|
+
switch (type) {
|
|
238
|
+
case 'number': return '42';
|
|
239
|
+
case 'date': return today;
|
|
240
|
+
case 'time': return '12:00';
|
|
241
|
+
case 'datetime-local': return `${today}T12:00`;
|
|
242
|
+
case 'month': return today.slice(0, 7);
|
|
243
|
+
case 'email': return 'check@example.com';
|
|
244
|
+
case 'url': return 'https://example.com';
|
|
245
|
+
case 'tel': return '5550100';
|
|
246
|
+
case 'text': case 'textarea': case 'search': case '': return PROBE_TEXT;
|
|
247
|
+
default: return null; // checkboxes, radios, files, colours: nothing to guess
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Fill in the rest of the form the probe text went into, the way a person
|
|
253
|
+
* would before pressing Add.
|
|
254
|
+
*
|
|
255
|
+
* Traced: a budget tracker's form also needs an amount. Only the description
|
|
256
|
+
* was typed, the browser refused the submit, and a working app was reported as
|
|
257
|
+
* "ADDING DOES NOT WORK" — the model then spent its fix rounds on code that
|
|
258
|
+
* worked. Filled: required fields, empty number fields (amounts are often
|
|
259
|
+
* checked in script, not marked required) and dropdowns left on a blank choice.
|
|
260
|
+
*/
|
|
261
|
+
async function fillTheRest(page, field) {
|
|
262
|
+
const marked = await field.evaluate((e) => {
|
|
263
|
+
if (!e.form) return false;
|
|
264
|
+
e.form.setAttribute('data-ucode-probe', '');
|
|
265
|
+
return true;
|
|
266
|
+
}).catch(() => false);
|
|
267
|
+
if (!marked) return;
|
|
268
|
+
|
|
269
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
270
|
+
const all = page.locator('form[data-ucode-probe] :is(input, select, textarea)');
|
|
271
|
+
const n = Math.min(await all.count().catch(() => 0), 20);
|
|
272
|
+
for (let i = 0; i < n; i++) {
|
|
273
|
+
const el = all.nth(i);
|
|
274
|
+
const info = await el.evaluate((e) => ({
|
|
275
|
+
tag: e.tagName.toLowerCase(),
|
|
276
|
+
type: (e.getAttribute('type') || (e.tagName === 'TEXTAREA' ? 'textarea' : '')).toLowerCase(),
|
|
277
|
+
empty: !e.value,
|
|
278
|
+
required: e.required,
|
|
279
|
+
})).catch(() => null);
|
|
280
|
+
if (!info?.empty || !(await el.isVisible().catch(() => false))) continue;
|
|
281
|
+
if (info.tag === 'select') {
|
|
282
|
+
await el.selectOption({ index: 1 }, { timeout: 2_000 }).catch(() => {});
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (!info.required && info.type !== 'number') continue;
|
|
286
|
+
const value = sampleFor(info.type, today);
|
|
287
|
+
if (value !== null) await el.fill(value, { timeout: 2_000 }).catch(() => {});
|
|
288
|
+
}
|
|
289
|
+
await page.locator('form[data-ucode-probe]').evaluate((f) => f.removeAttribute('data-ucode-probe')).catch(() => {});
|
|
290
|
+
}
|
|
291
|
+
|
|
235
292
|
async function tryAdd(page) {
|
|
236
293
|
const shows = () => page.evaluate((t) => document.body.innerText.includes(t), PROBE_TEXT).catch(() => false);
|
|
237
294
|
const settle = () => page.waitForTimeout(400);
|
|
@@ -279,6 +336,7 @@ async function tryAdd(page) {
|
|
|
279
336
|
}
|
|
280
337
|
const ok = await field.el.fill(PROBE_TEXT, { timeout: 2_000 }).then(() => true).catch(() => false);
|
|
281
338
|
if (!ok) return tried.length ? { tried, added: false, listApp } : null;
|
|
339
|
+
await fillTheRest(page, field.el);
|
|
282
340
|
await field.el.press('Enter', { timeout: 2_000 }).catch(() => {});
|
|
283
341
|
await settle();
|
|
284
342
|
tried.push(`typed "${PROBE_TEXT}" into ${field.hint ? `"${field.hint.slice(0, 30)}"` : 'the field'} and pressed Enter`);
|
package/src/tools/index.js
CHANGED
|
@@ -99,7 +99,11 @@ export const tools = [
|
|
|
99
99
|
'In "next-shadcn" every path in "files" goes under src/: a page is a route only at ' +
|
|
100
100
|
'<app>/src/app/page.tsx or <app>/src/app/<segment>/page.tsx, an API handler only at ' +
|
|
101
101
|
'<app>/src/app/api/<name>/route.ts, and components at <app>/src/components/<feature>/. ' +
|
|
102
|
-
'A page.tsx written anywhere else is an ordinary file the router never serves.'
|
|
102
|
+
'A page.tsx written anywhere else is an ordinary file the router never serves. ' +
|
|
103
|
+
'Scope: build what was asked, complete and well made - every feature someone using that kind of ' +
|
|
104
|
+
'app expects on first use (a tasks app adds, edits, ticks off, deletes, filters and remembers across ' +
|
|
105
|
+
'a reload) - and nothing from a different app (no timer, calendar or analytics bolted onto a tasks ' +
|
|
106
|
+
'app). Features nobody named cost minutes and are where builds break.',
|
|
103
107
|
parameters: {
|
|
104
108
|
type: 'object',
|
|
105
109
|
properties: {
|
package/src/tools/shell.js
CHANGED
|
@@ -57,6 +57,13 @@ const LOOKS_LIKE_INSTALL =
|
|
|
57
57
|
/** Opening a file or URL in the desktop browser: start, open, xdg-open, explorer. */
|
|
58
58
|
const OPENS_BROWSER = /^\s*(?:cd\s+[^&;|]+&&\s*)?(?:start(?:\s+"")?|open|xdg-open|explorer(?:\.exe)?|cmd(?:\.exe)?\s+\/c\s+start)\s+\S+\.html?\b|^\s*(?:start|open|xdg-open)\s+https?:\/\//i;
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Installing a browser to test the app. ucode already drives a real one after
|
|
62
|
+
* every change; a live quiz build spent two minutes on `npx @puppeteer/browsers
|
|
63
|
+
* install chrome` before it timed out.
|
|
64
|
+
*/
|
|
65
|
+
const INSTALLS_BROWSER = /@puppeteer\/browsers\s+install|\bplaywright(?:@\S+)?\s+install\b/i;
|
|
66
|
+
|
|
60
67
|
/**
|
|
61
68
|
* Any kill by program name rather than by process number.
|
|
62
69
|
*
|
|
@@ -672,6 +679,14 @@ export async function runCommand({ command, cwd, timeout_ms, background }, { onO
|
|
|
672
679
|
);
|
|
673
680
|
}
|
|
674
681
|
|
|
682
|
+
if (INSTALLS_BROWSER.test(command)) {
|
|
683
|
+
return result(
|
|
684
|
+
'Not run: ucode already opens the app in a real browser after every change and reports what it finds. ' +
|
|
685
|
+
'Fix what that report names; there is no browser to install.',
|
|
686
|
+
'not needed — ucode has a browser',
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
|
|
675
690
|
if (KILLS_BY_NAME.test(command)) {
|
|
676
691
|
const attempted = `running \`${command.trim().slice(0, 80)}\``;
|
|
677
692
|
try {
|