ucode-agent 1.62.2 → 1.62.4
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 +464 -464
- package/package.json +63 -63
- package/skills/build-app/DIGEST.md +154 -133
- package/skills/build-app/SKILL.md +22 -1
- package/src/core/loop.js +129 -16
- package/src/core/provider.js +12 -13
- package/src/tools/index.js +5 -1
- package/src/tools/shell.js +15 -0
package/src/core/loop.js
CHANGED
|
@@ -50,6 +50,7 @@ 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';
|
|
53
54
|
import { runDoctor } from './doctor.js';
|
|
54
55
|
import { JS_LOGIC } from './jslogic.js';
|
|
55
56
|
import { deploy } from '../tools/deploy.js';
|
|
@@ -109,6 +110,25 @@ const SILENT = new Set(['update_plan']);
|
|
|
109
110
|
/** How many rounds of "the type check found errors, fix them" one turn may take. */
|
|
110
111
|
const MAX_FIX_ROUNDS = 3;
|
|
111
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Rides on a new app's request, the last thing the model reads. In the system
|
|
115
|
+
* prompt and the skill alone, Flash-Lite still built "make me a tasks app" as a
|
|
116
|
+
* tasks app plus pomodoro timer, kanban board and analytics — twice running.
|
|
117
|
+
*/
|
|
118
|
+
const SCOPE_NOTE =
|
|
119
|
+
'(From ucode: if this asks for a new app, build it complete and well made, but as the one app asked ' +
|
|
120
|
+
'for - no extra views or tools such as timers, calendars, kanban boards, analytics, stats or an ' +
|
|
121
|
+
'editor for making your own, unless the request names them.)';
|
|
122
|
+
|
|
123
|
+
/** A request that may be for a new app. Loose on purpose: the note it adds says "if". */
|
|
124
|
+
const BUILD_ASK = /\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;
|
|
125
|
+
|
|
126
|
+
/** A file up to this many lines is sent whole on its first read, whatever slice was asked for. */
|
|
127
|
+
const WHOLE_READ_LINES = 1500;
|
|
128
|
+
|
|
129
|
+
/** Lookups in a row, during a fix round with nothing changed, before ucode says to stop reading. */
|
|
130
|
+
const LOOKUP_NUDGE = 5;
|
|
131
|
+
|
|
112
132
|
/** Files worth checking after they change. */
|
|
113
133
|
/** A file with a page in it — something a browser can be pointed at. */
|
|
114
134
|
const PAGE = /\.(?:html?|tsx|jsx)$/i;
|
|
@@ -621,6 +641,15 @@ function systemPrompt({ cwd, skills, mode, check, map, memory }) {
|
|
|
621
641
|
'already contains it. Do not re-check work the checks have already reported on.',
|
|
622
642
|
'Fast is not sloppy: it is the same work with the waiting taken out.',
|
|
623
643
|
'',
|
|
644
|
+
'BUILD WHAT WAS ASKED - ALL OF IT, AND NOTHING ELSE. A short request ("a tasks',
|
|
645
|
+
'app", "a quiz", "a snake game") still gets a complete, well-made app: every',
|
|
646
|
+
'feature someone using that kind of app expects on first use. A tasks app adds,',
|
|
647
|
+
'edits, ticks off, deletes, filters and remembers across a reload. It does NOT',
|
|
648
|
+
'also get a pomodoro timer, a calendar, an analytics dashboard, stats charts or a',
|
|
649
|
+
'shortcuts panel - those are other apps. Extra views are where builds break, and',
|
|
650
|
+
'each one costs the user minutes. One screen that does its job perfectly beats',
|
|
651
|
+
'four that half work. An extra worth having: offer it in one line at the end.',
|
|
652
|
+
'',
|
|
624
653
|
'DESIGN IT BEFORE YOU TYPE IT. Fast means fewer round trips. It does not mean a',
|
|
625
654
|
'default theme, and an app that goes out in the palette its starter came with is',
|
|
626
655
|
'not a fast build, it is an undesigned one. There is no design pass after the',
|
|
@@ -632,7 +661,7 @@ function systemPrompt({ cwd, skills, mode, check, map, memory }) {
|
|
|
632
661
|
' - An ACCENT that is not the one the starter shipped with.',
|
|
633
662
|
' - ONE memorable thing this app has that other apps do not: a colour, a type',
|
|
634
663
|
' move, a texture, one interaction. Exactly one. It is the difference between',
|
|
635
|
-
' a design and a theme.',
|
|
664
|
+
' a design and a theme - a design choice, never an extra feature or view.',
|
|
636
665
|
'Name the tone and the accent in your opening line, so they are settled before any',
|
|
637
666
|
'file exists: "I will build Tide - a tasks app in one HTML file, calm, warm grey',
|
|
638
667
|
'with a single amber accent." That is not narrating a plan, that is the decision.',
|
|
@@ -1205,6 +1234,9 @@ export class Agent {
|
|
|
1205
1234
|
this.appName = null;
|
|
1206
1235
|
this.appTemplate = null;
|
|
1207
1236
|
this.reads = new Map();
|
|
1237
|
+
this.fixing = false;
|
|
1238
|
+
this.lookups = 0;
|
|
1239
|
+
this.nudgedAt = 0;
|
|
1208
1240
|
this.declines = 0;
|
|
1209
1241
|
this.apps = [];
|
|
1210
1242
|
this.wrote = new Map();
|
|
@@ -1216,9 +1248,8 @@ export class Agent {
|
|
|
1216
1248
|
// model answered: asked for a dark mode toggle on its second turn, a live
|
|
1217
1249
|
// run re-read two files and repeated its first turn's summary instead.
|
|
1218
1250
|
this.autoLoad(input);
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
: { role: 'user', content: input });
|
|
1251
|
+
const request = images.length ? { role: 'user', content: input, images } : { role: 'user', content: input };
|
|
1252
|
+
this.push(request);
|
|
1222
1253
|
|
|
1223
1254
|
if (!this.session.title || this.session.title === 'Untitled') {
|
|
1224
1255
|
this.session.title = titleFrom(input);
|
|
@@ -1231,6 +1262,7 @@ export class Agent {
|
|
|
1231
1262
|
// Nothing to look up in an empty folder, so those tools do not go out with
|
|
1232
1263
|
// the request. Decided per turn: the moment there is code, they are back.
|
|
1233
1264
|
this.fresh = !hasCode(this.map);
|
|
1265
|
+
if (this.fresh || BUILD_ASK.test(input)) request.content = `${input}\n\n${SCOPE_NOTE}`;
|
|
1234
1266
|
this.wantsWeb = WANTS_WEB.test(input);
|
|
1235
1267
|
await this.persist();
|
|
1236
1268
|
|
|
@@ -1246,7 +1278,8 @@ export class Agent {
|
|
|
1246
1278
|
this.abort = new AbortController();
|
|
1247
1279
|
this.endedSilently = false;
|
|
1248
1280
|
|
|
1249
|
-
|
|
1281
|
+
// Kept on the agent too: the hand-over says how long the build took.
|
|
1282
|
+
const turnStarted = (this.turnStarted = Date.now());
|
|
1250
1283
|
let finished = false;
|
|
1251
1284
|
this.ui.turnStart?.();
|
|
1252
1285
|
try {
|
|
@@ -1485,6 +1518,7 @@ export class Agent {
|
|
|
1485
1518
|
const problems = await this.autoCheck();
|
|
1486
1519
|
if (problems) {
|
|
1487
1520
|
fixRounds++;
|
|
1521
|
+
this.fixing = true;
|
|
1488
1522
|
if (reply.text) this.push({ role: 'assistant', content: reply.text });
|
|
1489
1523
|
this.push({
|
|
1490
1524
|
role: 'user',
|
|
@@ -1570,6 +1604,7 @@ export class Agent {
|
|
|
1570
1604
|
if (handed?.done) return;
|
|
1571
1605
|
if (handed?.problems && fixRounds < MAX_FIX_ROUNDS) {
|
|
1572
1606
|
fixRounds++;
|
|
1607
|
+
this.fixing = true;
|
|
1573
1608
|
this.push({
|
|
1574
1609
|
role: 'user',
|
|
1575
1610
|
content:
|
|
@@ -1592,8 +1627,7 @@ export class Agent {
|
|
|
1592
1627
|
`${modelName()} produced invalid tool arguments ${argRetries} times running ` +
|
|
1593
1628
|
'and could not correct itself.',
|
|
1594
1629
|
fix:
|
|
1595
|
-
'Say what you want more concretely, or /model to another one
|
|
1596
|
-
'Code and Nemotron 3.5 Lightning are both steadier with tool arguments.',
|
|
1630
|
+
'Say what you want more concretely, or /model to another one.',
|
|
1597
1631
|
});
|
|
1598
1632
|
}
|
|
1599
1633
|
} else {
|
|
@@ -1643,8 +1677,16 @@ export class Agent {
|
|
|
1643
1677
|
const noted = (call) => {
|
|
1644
1678
|
if (FILE_WRITES.has(call.name)) {
|
|
1645
1679
|
for (const p of pathsOf(call)) { this.touched.add(p); this.sinceCheck.add(p); this.forgetReads(p); }
|
|
1680
|
+
this.lookups = 0;
|
|
1681
|
+
this.nudgedAt = 0;
|
|
1682
|
+
} else if (PARALLEL_SAFE.has(call.name)) {
|
|
1683
|
+
this.lookups = (this.lookups ?? 0) + 1;
|
|
1684
|
+
}
|
|
1685
|
+
if (call.name === 'run_command' || call.name === 'run_commands') {
|
|
1686
|
+
this.ranSomething = true;
|
|
1687
|
+
// A command can rewrite any file, so no whole read is still known to be current.
|
|
1688
|
+
for (const key of this.reads?.keys() ?? []) if (key.endsWith('#whole')) this.reads.delete(key);
|
|
1646
1689
|
}
|
|
1647
|
-
if (call.name === 'run_command' || call.name === 'run_commands') this.ranSomething = true;
|
|
1648
1690
|
};
|
|
1649
1691
|
|
|
1650
1692
|
if (group.length > 1) {
|
|
@@ -1750,13 +1792,29 @@ export class Agent {
|
|
|
1750
1792
|
const server = serversReadySince(since).at(-1);
|
|
1751
1793
|
if (!server || (this.opened ??= new Set()).has(server.url)) return;
|
|
1752
1794
|
this.opened.add(server.url);
|
|
1795
|
+
if (this.openInBrowser(server.url)) this.ui.note(`Opened ${server.url} in your browser`);
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
/**
|
|
1799
|
+
* Show a URL or a local page in the desktop browser. False when it did not.
|
|
1800
|
+
*
|
|
1801
|
+
* A file goes to explorer on Windows, not `cmd /c start`: the folder name
|
|
1802
|
+
* comes from the model, and an & in it would be a second command to cmd.
|
|
1803
|
+
*/
|
|
1804
|
+
openInBrowser(target) {
|
|
1805
|
+
if (!this.full || process.env.UCODE_OPEN === '0') return false;
|
|
1806
|
+
const web = /^https?:\/\//.test(target);
|
|
1807
|
+
// Only pages inside the project: a UNC path would have explorer reach out to another machine.
|
|
1808
|
+
const rel = web ? '' : path.relative(this.cwd, target);
|
|
1809
|
+
if (!web && (!rel || rel.startsWith('..') || path.isAbsolute(rel))) return false;
|
|
1753
1810
|
const [cmd, args] = process.platform === 'win32'
|
|
1754
|
-
? ['cmd', ['/c', 'start', '',
|
|
1755
|
-
: [process.platform === 'darwin' ? 'open' : 'xdg-open', [
|
|
1811
|
+
? (web ? ['cmd', ['/c', 'start', '', target]] : ['explorer.exe', [target]])
|
|
1812
|
+
: [process.platform === 'darwin' ? 'open' : 'xdg-open', [target]];
|
|
1756
1813
|
try {
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1814
|
+
// A missing opener (no xdg-open) fails later, as an event; unheard, it would crash ucode.
|
|
1815
|
+
spawn(cmd, args, { stdio: 'ignore', detached: true, windowsHide: true }).on('error', () => {}).unref();
|
|
1816
|
+
return true;
|
|
1817
|
+
} catch { return false; /* no browser to open — the link is in the answer */ }
|
|
1760
1818
|
}
|
|
1761
1819
|
|
|
1762
1820
|
cmdStats() {
|
|
@@ -1862,7 +1920,34 @@ export class Agent {
|
|
|
1862
1920
|
const found = /^(\d+) problem/.exec(out.summary ?? '');
|
|
1863
1921
|
this.ui.runStat?.(found ? `${found[1]} to fix` : 'clean');
|
|
1864
1922
|
}
|
|
1865
|
-
this.push({ role: 'tool', toolCallId: call.id, name: call.name, content: out.content + this.stuckNote(call, { out }) });
|
|
1923
|
+
this.push({ role: 'tool', toolCallId: call.id, name: call.name, content: out.content + this.stuckNote(call, { out }) + this.lookupNote(call) });
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
/**
|
|
1927
|
+
* Reading on and on while ucode's list of errors waits. A live fix round
|
|
1928
|
+
* read a 1000-line app.js in seventeen slices, changed nothing, and ran the
|
|
1929
|
+
* free key into its per-minute limit: every read resends the conversation.
|
|
1930
|
+
*/
|
|
1931
|
+
lookupNote(call) {
|
|
1932
|
+
if (!this.fixing || !PARALLEL_SAFE.has(call.name)) return '';
|
|
1933
|
+
if (this.lookups < LOOKUP_NUDGE || this.lookups - this.nudgedAt < LOOKUP_NUDGE) return '';
|
|
1934
|
+
this.nudgedAt = this.lookups;
|
|
1935
|
+
return `\n\nSTOP reading - that is ${this.lookups} lookups since ucode listed the errors, and nothing has been ` +
|
|
1936
|
+
'changed yet. What you need is already above. Make the fix now with edit_file or multi_edit; ucode ' +
|
|
1937
|
+
'checks the app again straight after.';
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
/** Lines in a file small enough to send whole in one read, or null. */
|
|
1941
|
+
async wholeSize(file) {
|
|
1942
|
+
try {
|
|
1943
|
+
const text = await readFile(path.resolve(this.cwd, file), 'utf8');
|
|
1944
|
+
if (text.includes('\0')) return null;
|
|
1945
|
+
const lines = text.split('\n').length;
|
|
1946
|
+
// The line-number gutter adds about eight characters a line to what read_file sends.
|
|
1947
|
+
return lines <= WHOLE_READ_LINES && text.length + lines * 8 <= MAX_FILE_OUTPUT ? { lines } : null;
|
|
1948
|
+
} catch {
|
|
1949
|
+
return null;
|
|
1950
|
+
}
|
|
1866
1951
|
}
|
|
1867
1952
|
|
|
1868
1953
|
/**
|
|
@@ -2018,6 +2103,27 @@ export class Agent {
|
|
|
2018
2103
|
// refused as "unchanged", and a live build spent sixty steps being told so.
|
|
2019
2104
|
if (call.name === 'read_file' && call.args?.path) {
|
|
2020
2105
|
const file = String(call.args.path);
|
|
2106
|
+
|
|
2107
|
+
// A file that fits is sent whole the first time, and not again until
|
|
2108
|
+
// something writes to it. Slices were the loop: a live fix round read one
|
|
2109
|
+
// app.js in seventeen of them, never edited, and hit the per-minute limit.
|
|
2110
|
+
// The lead only — a worker has its own conversation, without the lead's reads.
|
|
2111
|
+
if (offered === this.offering) {
|
|
2112
|
+
const whole = `${fileKey(file)}#whole`;
|
|
2113
|
+
if (this.reads.has(whole)) {
|
|
2114
|
+
return {
|
|
2115
|
+
content: `All of ${file} is already above: you read the whole file and nothing has written to it ` +
|
|
2116
|
+
'since. Use that text instead of reading it again. If something in it needs changing, change it ' +
|
|
2117
|
+
'now with edit_file or multi_edit.',
|
|
2118
|
+
summary: 'unchanged, already read in full',
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
// Marked before the await, so two reads of one file in a parallel batch send it once.
|
|
2122
|
+
this.reads.set(whole, 1);
|
|
2123
|
+
const size = await this.wholeSize(file);
|
|
2124
|
+
if (size) call.args = { ...call.args, offset: 1, limit: size.lines };
|
|
2125
|
+
else this.reads.delete(whole);
|
|
2126
|
+
}
|
|
2021
2127
|
const key = `${fileKey(file)}#${Number(call.args.offset) || 1}:${Number(call.args.limit) || 0}`;
|
|
2022
2128
|
const seen = (this.reads ??= new Map()).get(key) ?? 0;
|
|
2023
2129
|
if (seen >= 2) {
|
|
@@ -2597,11 +2703,16 @@ export class Agent {
|
|
|
2597
2703
|
const made = calls.find((c) => c.name === 'create_app' && c.args?.name)?.args.name ?? this.appName;
|
|
2598
2704
|
const name = String(made ?? path.basename(app)).trim();
|
|
2599
2705
|
this.appName = name;
|
|
2600
|
-
const
|
|
2601
|
-
const
|
|
2706
|
+
const page = path.join(app, 'index.html');
|
|
2707
|
+
const link = pathToFileURL(page).href;
|
|
2708
|
+
// "Quiz is done in 42s": how long the whole build took, from the request.
|
|
2709
|
+
const took = this.turnStarted ? ` in ${formatDuration(Date.now() - this.turnStarted)}` : '';
|
|
2710
|
+
const text = `${name} is done${took}. Open it here: ${link}\n\n(or open \`${show}/index.html\` in your browser)`;
|
|
2602
2711
|
this.push({ role: 'assistant', content: text });
|
|
2603
2712
|
this.ui.assistant(text, { closing: true });
|
|
2604
2713
|
await this.persist();
|
|
2714
|
+
// The app pops up on its own; UCODE_OPEN=0 keeps it to the link.
|
|
2715
|
+
this.openInBrowser(page);
|
|
2605
2716
|
return { done: true };
|
|
2606
2717
|
}
|
|
2607
2718
|
|
|
@@ -2788,6 +2899,8 @@ export class Agent {
|
|
|
2788
2899
|
this.ui.stopSpinner();
|
|
2789
2900
|
if (result.folded) {
|
|
2790
2901
|
this.working = result.messages;
|
|
2902
|
+
// What was read is summarised away now, so it may be read again.
|
|
2903
|
+
this.reads = new Map();
|
|
2791
2904
|
this.ui.note(
|
|
2792
2905
|
`folded ${result.droppedCount} earlier messages into a summary ` +
|
|
2793
2906
|
'(the full history is still saved in this session)'
|
package/src/core/provider.js
CHANGED
|
@@ -500,9 +500,8 @@ export function explain(err, id) {
|
|
|
500
500
|
: `Too many requests for ${modelName(id)} just now${wait ? ` — clear in ${wait}` : ''}.`,
|
|
501
501
|
fix: daily
|
|
502
502
|
? `It resets ${resetAt ? `at ${resetAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}` : 'once a day'}. ` +
|
|
503
|
-
'
|
|
504
|
-
: 'ucode waits these out on its own
|
|
505
|
-
'clears in seconds; /model moves to a quieter one.',
|
|
503
|
+
'Nothing to do until then — the key is fine.'
|
|
504
|
+
: 'ucode waits these out on its own, usually for a few seconds. /model moves to another one.',
|
|
506
505
|
detail: { retryAfter, daily, resetAt: resetAt?.getTime() ?? null },
|
|
507
506
|
cause: err,
|
|
508
507
|
});
|
|
@@ -596,10 +595,7 @@ export function explain(err, id) {
|
|
|
596
595
|
kind: 'timeout',
|
|
597
596
|
attempted,
|
|
598
597
|
failed: `${modelName(id)} sent nothing back in time — the provider dropped the request.`,
|
|
599
|
-
fix:
|
|
600
|
-
'Free endpoints stall under load, and the largest reasoning models are the ' +
|
|
601
|
-
'first to. ucode already retried. If it keeps happening, /model to Nemotron ' +
|
|
602
|
-
'3.5 Lightning or North Mini Code, which answer sooner.',
|
|
598
|
+
fix: 'ucode already retried. Google is busy right now — send again, or /model to another one.',
|
|
603
599
|
detail: { status },
|
|
604
600
|
cause: err,
|
|
605
601
|
});
|
|
@@ -609,7 +605,7 @@ export function explain(err, id) {
|
|
|
609
605
|
return new Failure({
|
|
610
606
|
kind: 'server',
|
|
611
607
|
attempted,
|
|
612
|
-
failed: `
|
|
608
|
+
failed: `Google's servers are busy right now (HTTP ${status}). That is their side, not yours.`,
|
|
613
609
|
fix: 'Wait a few seconds and send again. If it persists, /model to another one.',
|
|
614
610
|
cause: err,
|
|
615
611
|
});
|
|
@@ -627,11 +623,14 @@ export function explain(err, id) {
|
|
|
627
623
|
return new Failure({
|
|
628
624
|
kind: 'network',
|
|
629
625
|
attempted,
|
|
630
|
-
|
|
626
|
+
// No DNS answer: the machine is offline, and saying so
|
|
627
|
+
// beats a raw "getaddrinfo ENOTFOUND" on a demo screen.
|
|
628
|
+
failed: /ENOTFOUND|EAI_AGAIN|getaddrinfo/i.test(raw)
|
|
629
|
+
? 'ucode could not reach Google — this computer looks offline.'
|
|
630
|
+
: `The connection to Google dropped: ${raw}`,
|
|
631
631
|
fix:
|
|
632
|
-
'ucode retries this by itself. If it keeps happening, check
|
|
633
|
-
'VPN and any
|
|
634
|
-
'a long think on a busy free endpoint is the usual cause.',
|
|
632
|
+
'ucode retries this by itself. If it keeps happening, check the Wi-Fi, ' +
|
|
633
|
+
'VPN and any proxy (HTTPS_PROXY), then send again.',
|
|
635
634
|
cause: err,
|
|
636
635
|
});
|
|
637
636
|
}
|
|
@@ -785,7 +784,7 @@ async function streamed(request, opts, id) {
|
|
|
785
784
|
kind: 'timeout',
|
|
786
785
|
attempted: `asking ${modelName(id)} for a reply`,
|
|
787
786
|
failed: `${modelName(id)} went silent for ${Math.round(limit() / 1000)}s, so ucode stopped waiting.`,
|
|
788
|
-
fix: 'ucode asks again by itself. If it keeps freezing, /model to
|
|
787
|
+
fix: 'ucode asks again by itself. If it keeps freezing, /model to another one.',
|
|
789
788
|
detail: { stalled: true, handed: handed.size },
|
|
790
789
|
cause,
|
|
791
790
|
});
|
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 {
|