ucode-agent 1.44.0 → 1.47.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 +6 -5
- package/package.json +5 -3
- package/src/core/csscheck.js +77 -0
- package/src/core/history.js +6 -1
- package/src/core/jslogic.js +69 -0
- package/src/core/loop.js +3005 -2821
- package/src/core/provider.js +931 -924
- package/src/core/relink.js +2 -1
- package/src/tools/blocks.js +2 -2
- package/src/tools/browser.js +546 -496
- package/src/tools/files.js +750 -743
- package/src/tools/index.js +13 -10
- package/src/tools/rename.js +2 -2
- package/src/tools/scaffold.js +439 -424
- package/src/tools/shared.js +126 -0
- package/src/tools/shell.js +844 -818
- package/src/ui/plain.js +353 -353
- package/src/ui/screen.js +1699 -1692
- package/src/ui/theme.js +814 -776
- package/ucode.js +1 -1
package/src/tools/shared.js
CHANGED
|
@@ -75,6 +75,52 @@ export async function confirm(action, detail, risk = 'write') {
|
|
|
75
75
|
if (!(await asker({ action, detail, risk }))) throw new Declined(action);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// The request behind the call
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* What the user asked for this turn, in their own words.
|
|
84
|
+
*
|
|
85
|
+
* A tool cannot normally see the request that led to it, and for almost
|
|
86
|
+
* everything that is right — a tool should act on its arguments. The starter
|
|
87
|
+
* is the exception. "Make me a plain HTML app" is not a preference to be
|
|
88
|
+
* weighed against the convenience of a framework, but the model weighs it
|
|
89
|
+
* anyway: it reads "app", reaches for the starter with the component library
|
|
90
|
+
* in it, and the user waits through an npm install they explicitly said they
|
|
91
|
+
* did not want. Keeping the request here lets create_app check the instruction
|
|
92
|
+
* rather than trust the argument it was handed.
|
|
93
|
+
*/
|
|
94
|
+
let request = '';
|
|
95
|
+
|
|
96
|
+
export function setRequest(text) {
|
|
97
|
+
request = String(text ?? '');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** "plain html", "vanilla js", "simple static page". */
|
|
101
|
+
const WANTS_PLAIN = /\b(?:plain|pure|vanilla|static|simple|basic|raw|just)\s+(?:html|js|javascript|css)\b/i;
|
|
102
|
+
/** "an html app", "one html page". */
|
|
103
|
+
const HTML_THING = /\bhtml\s+(?:app|page|site|website|file|thing)\b/i;
|
|
104
|
+
/** "no framework", "without react", "don't use next". */
|
|
105
|
+
const NO_FRAMEWORK =
|
|
106
|
+
/\b(?:no|without|not?\s+use|don'?t\s+use|do\s+not\s+use|skip)\s+(?:a\s+|any\s+)?(?:framework|frameworks|react|next\.?js|next|npm|node|build\s+step|bundler)\b/i;
|
|
107
|
+
/** Naming one on purpose outranks every hint above. */
|
|
108
|
+
const NAMES_FRAMEWORK = /\b(?:next\.?js|nextjs|react|tailwind|shadcn|typescript|database|api\s+routes?|server\s+side|auth)\b/i;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Did the user rule out a framework in so many words?
|
|
112
|
+
*
|
|
113
|
+
* Saying "no react" settles it on its own. Asking for "an html app" settles it
|
|
114
|
+
* only when the same breath does not also ask for Next.js — "export this
|
|
115
|
+
* Next.js app as static html" names the framework on purpose, and a request
|
|
116
|
+
* that specific is not one to overrule.
|
|
117
|
+
*/
|
|
118
|
+
export function askedForPlainHtml() {
|
|
119
|
+
if (NO_FRAMEWORK.test(request)) return true;
|
|
120
|
+
if (!WANTS_PLAIN.test(request) && !HTML_THING.test(request)) return false;
|
|
121
|
+
return !NAMES_FRAMEWORK.test(request);
|
|
122
|
+
}
|
|
123
|
+
|
|
78
124
|
// ---------------------------------------------------------------------------
|
|
79
125
|
// Paths
|
|
80
126
|
// ---------------------------------------------------------------------------
|
|
@@ -341,3 +387,83 @@ export async function walk(base, { includeSkipped = false, limit = 20_000 } = {}
|
|
|
341
387
|
|
|
342
388
|
/** How many directories, or files, are read at the same time. */
|
|
343
389
|
export const WALK_WIDTH = 32;
|
|
390
|
+
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
// Files as ucode last left them
|
|
393
|
+
// ---------------------------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* abs path -> the modification time ucode last saw, after reading or writing it.
|
|
397
|
+
*
|
|
398
|
+
* An agent that rewrites a whole file is trusting that the file still says what
|
|
399
|
+
* it said when it was read. That holds right up until someone has the editor
|
|
400
|
+
* open beside the terminal, saves a change mid-turn, and has it overwritten
|
|
401
|
+
* without a word — the one failure here that costs work nobody can get back,
|
|
402
|
+
* since the undo only holds what the turn itself replaced.
|
|
403
|
+
*
|
|
404
|
+
* So every read and every write leaves a stamp, and a whole-file overwrite
|
|
405
|
+
* checks it first. Edits do not need the check: they read the file again a
|
|
406
|
+
* moment before they touch it, and match their old_string against what is
|
|
407
|
+
* actually there.
|
|
408
|
+
*/
|
|
409
|
+
const known = new Map();
|
|
410
|
+
|
|
411
|
+
/** Record a file as ucode now knows it. Never throws: a missing stamp only costs the check. */
|
|
412
|
+
export async function noteFile(abs) {
|
|
413
|
+
try {
|
|
414
|
+
known.set(abs, (await fs.stat(abs)).mtimeMs);
|
|
415
|
+
} catch {
|
|
416
|
+
known.delete(abs);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Write a file and stamp it, so the next overwrite knows this change was ours.
|
|
422
|
+
*
|
|
423
|
+
* @param {string} abs
|
|
424
|
+
* @param {string} data
|
|
425
|
+
* @param {BufferEncoding} [encoding]
|
|
426
|
+
*/
|
|
427
|
+
export async function writeTracked(abs, data, encoding = 'utf8') {
|
|
428
|
+
await fs.writeFile(abs, data, encoding);
|
|
429
|
+
await noteFile(abs);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Forget a file, so the next write to it goes through unchallenged. */
|
|
433
|
+
export function forgetFile(abs) {
|
|
434
|
+
known.delete(abs);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Refuse to overwrite a file that somebody else has changed since ucode read it.
|
|
439
|
+
*
|
|
440
|
+
* Only ever fires once per file: the stamp is dropped on the way out, so the
|
|
441
|
+
* model reads the file again — which is what the message tells it to do — and
|
|
442
|
+
* the next attempt writes normally. A file ucode has never seen is not guarded,
|
|
443
|
+
* because there is nothing to compare it against and a first write is not a
|
|
444
|
+
* clobber.
|
|
445
|
+
*/
|
|
446
|
+
export async function assertUnchanged(abs, show) {
|
|
447
|
+
const seen = known.get(abs);
|
|
448
|
+
if (seen === undefined) return;
|
|
449
|
+
|
|
450
|
+
let now;
|
|
451
|
+
try {
|
|
452
|
+
now = (await fs.stat(abs)).mtimeMs;
|
|
453
|
+
} catch {
|
|
454
|
+
return; // gone, or unreadable — the write itself will say so
|
|
455
|
+
}
|
|
456
|
+
// Filesystems report times at different resolutions; a millisecond of slack
|
|
457
|
+
// costs nothing and stops a same-second write reading as someone else's.
|
|
458
|
+
if (Math.abs(now - seen) < 1) return;
|
|
459
|
+
|
|
460
|
+
known.delete(abs);
|
|
461
|
+
throw new ToolFailure({
|
|
462
|
+
kind: 'changed_on_disk',
|
|
463
|
+
attempted: `overwriting ${show}`,
|
|
464
|
+
failed: `${show} has changed on disk since you last read it — someone else has edited it.`,
|
|
465
|
+
fix:
|
|
466
|
+
`Read ${show} again, fold your change into what is there now, and write it once more. ` +
|
|
467
|
+
'Writing the version you had would throw their edit away.',
|
|
468
|
+
});
|
|
469
|
+
}
|