ucode-agent 1.2.0 → 1.3.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 +42 -8
- package/package.json +1 -1
- package/src/core/context.js +151 -0
- package/src/core/loop.js +376 -25
- package/src/core/provider.js +18 -3
- package/src/tools/files.js +173 -16
- package/src/tools/index.js +445 -394
- package/src/tools/shell.js +141 -0
- package/src/ui/plain.js +330 -325
- package/src/ui/screen.js +8 -2
- package/src/ui/theme.js +18 -0
package/src/tools/shell.js
CHANGED
|
@@ -82,6 +82,10 @@ export function childEnv(base = process.env) {
|
|
|
82
82
|
npm_config_fund: 'false',
|
|
83
83
|
npm_config_audit: 'false',
|
|
84
84
|
npm_config_update_notifier: 'false',
|
|
85
|
+
// Take a package from the local cache when it is there, instead of asking
|
|
86
|
+
// the registry whether a newer copy exists first. Installing the same
|
|
87
|
+
// framework for the second app in a day goes from network-bound to disk-bound.
|
|
88
|
+
npm_config_prefer_offline: 'true',
|
|
85
89
|
NEXT_TELEMETRY_DISABLED: '1',
|
|
86
90
|
NO_COLOR: '1',
|
|
87
91
|
FORCE_COLOR: '0',
|
|
@@ -318,6 +322,137 @@ function startServer(command, workdir, { env } = {}) {
|
|
|
318
322
|
});
|
|
319
323
|
}
|
|
320
324
|
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
// Installing in the background
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Installs already running, by directory.
|
|
331
|
+
*
|
|
332
|
+
* The moment a package.json with dependencies is written, its install starts
|
|
333
|
+
* in the background — while the model is still writing the components. By the
|
|
334
|
+
* time it asks to install, build or start the app, the install is usually done
|
|
335
|
+
* or nearly so, and whatever wait is left is the remainder rather than the
|
|
336
|
+
* whole thing.
|
|
337
|
+
*/
|
|
338
|
+
const installs = new Map();
|
|
339
|
+
|
|
340
|
+
/** The package manager a project already uses, going by its lockfile. */
|
|
341
|
+
export function packageManagerFor(dir) {
|
|
342
|
+
const has = (f) => { try { statSync(path.join(dir, f)); return true; } catch { return false; } };
|
|
343
|
+
if (has('pnpm-lock.yaml')) return 'pnpm';
|
|
344
|
+
if (has('yarn.lock')) return 'yarn';
|
|
345
|
+
if (has('bun.lockb') || has('bun.lock')) return 'bun';
|
|
346
|
+
// npm by default, deliberately: pnpm 10+ refuses to run install scripts
|
|
347
|
+
// without an interactive approval, which fails the install outright here.
|
|
348
|
+
return 'npm';
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function runInstall(dir) {
|
|
352
|
+
const pm = packageManagerFor(dir);
|
|
353
|
+
const command = pm === 'npm' ? 'npm install --no-audit --no-fund' : `${pm} install`;
|
|
354
|
+
const started = Date.now();
|
|
355
|
+
|
|
356
|
+
const promise = new Promise((resolve) => {
|
|
357
|
+
let output = '';
|
|
358
|
+
let child;
|
|
359
|
+
try {
|
|
360
|
+
child = spawn(command, {
|
|
361
|
+
cwd: dir, shell: true, windowsHide: true,
|
|
362
|
+
stdio: ['ignore', 'pipe', 'pipe'], env: childEnv(),
|
|
363
|
+
});
|
|
364
|
+
} catch (err) {
|
|
365
|
+
resolve({ code: -1, output: err.message, command, seconds: 0 });
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const take = (chunk) => { if (output.length < MAX_OUTPUT * 2) output += stripAnsi(chunk.toString()); };
|
|
369
|
+
child.stdout?.on('data', take);
|
|
370
|
+
child.stderr?.on('data', take);
|
|
371
|
+
const timer = setTimeout(() => killTree(child.pid), INSTALL_TIMEOUT);
|
|
372
|
+
child.on('close', (code) => {
|
|
373
|
+
clearTimeout(timer);
|
|
374
|
+
resolve({ code, output: output.trim(), command, seconds: Math.round((Date.now() - started) / 1000) });
|
|
375
|
+
});
|
|
376
|
+
child.on('error', (err) => {
|
|
377
|
+
clearTimeout(timer);
|
|
378
|
+
resolve({ code: -1, output: err.message, command, seconds: 0 });
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const entry = { promise, stale: false };
|
|
383
|
+
installs.set(dir, entry);
|
|
384
|
+
// If package.json changed again while this was running, go once more.
|
|
385
|
+
promise.then(() => {
|
|
386
|
+
if (installs.get(dir) !== entry) return;
|
|
387
|
+
if (entry.stale) runInstall(dir);
|
|
388
|
+
else installs.delete(dir);
|
|
389
|
+
});
|
|
390
|
+
return entry;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Called whenever a package.json is written. Starts an install if it declares
|
|
395
|
+
* dependencies, or marks a running one to go again with the new list.
|
|
396
|
+
*/
|
|
397
|
+
export function packageJsonWritten(file, content) {
|
|
398
|
+
let pkg;
|
|
399
|
+
try { pkg = JSON.parse(content); } catch { return; }
|
|
400
|
+
const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
|
|
401
|
+
if (Object.keys(deps).length === 0) return;
|
|
402
|
+
|
|
403
|
+
const dir = path.dirname(file);
|
|
404
|
+
const running = installs.get(dir);
|
|
405
|
+
if (running) running.stale = true;
|
|
406
|
+
else runInstall(dir);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** The install running for this directory or any folder above it, if one is. */
|
|
410
|
+
function installFor(dir) {
|
|
411
|
+
let at = path.resolve(dir);
|
|
412
|
+
for (;;) {
|
|
413
|
+
if (installs.has(at)) return { dir: at, entry: installs.get(at) };
|
|
414
|
+
const up = path.dirname(at);
|
|
415
|
+
if (up === at) return null;
|
|
416
|
+
at = up;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const PLAIN_INSTALL = /^\s*(?:npm\s+(?:i|install)|pnpm\s+(?:i|install)|yarn(?:\s+install)?|bun\s+(?:i|install))(?:\s+--?[\w-]+(?:=\S+)?)*\s*$/i;
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Wait for a background install before running something that needs it — and
|
|
424
|
+
* if the command IS that install, hand back the background one's result
|
|
425
|
+
* instead of doing it twice.
|
|
426
|
+
*/
|
|
427
|
+
async function awaitInstall(command, workdir, onOutput) {
|
|
428
|
+
// Models often write `cd app && npm run build` instead of passing cwd, so the
|
|
429
|
+
// directory a command really runs in is read off the front of it.
|
|
430
|
+
const cd = /^\s*cd\s+(?:\/d\s+)?("?)([^"&|;]+?)\1\s*(?:&&|;)\s*/i.exec(command);
|
|
431
|
+
const dir = cd ? path.resolve(workdir.abs, cd[2].trim()) : path.resolve(workdir.abs);
|
|
432
|
+
const rest = cd ? command.slice(cd[0].length) : command;
|
|
433
|
+
|
|
434
|
+
const found = installFor(dir);
|
|
435
|
+
if (!found) return null;
|
|
436
|
+
|
|
437
|
+
onOutput?.(['waiting for the install that started when package.json was written']);
|
|
438
|
+
let done = await found.entry.promise;
|
|
439
|
+
// It may have been restarted for a newer package.json; wait for that too.
|
|
440
|
+
while (installs.get(found.dir) && installs.get(found.dir) !== found.entry) {
|
|
441
|
+
done = await installs.get(found.dir).promise;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (!PLAIN_INSTALL.test(rest) || path.resolve(found.dir) !== dir) return null;
|
|
445
|
+
|
|
446
|
+
const out = result(
|
|
447
|
+
`The install already ran in the background as soon as package.json was written ` +
|
|
448
|
+
`(\`${done.command}\`, ${done.seconds}s).\n\nexit code: ${done.code}\n\n${done.output || '(no output)'}`,
|
|
449
|
+
`already installed in the background · exit ${done.code} · ${done.seconds}s`
|
|
450
|
+
);
|
|
451
|
+
out.exitCode = done.code;
|
|
452
|
+
if (done.code !== 0) out.output = tail(done.output);
|
|
453
|
+
return out;
|
|
454
|
+
}
|
|
455
|
+
|
|
321
456
|
// ---------------------------------------------------------------------------
|
|
322
457
|
// run_command
|
|
323
458
|
// ---------------------------------------------------------------------------
|
|
@@ -355,6 +490,12 @@ export async function runCommand({ command, cwd, timeout_ms, background }, { onO
|
|
|
355
490
|
const env = childEnv();
|
|
356
491
|
const server = LOOKS_LIKE_SERVER.test(command);
|
|
357
492
|
|
|
493
|
+
// Anything run where a background install is still going waits for it —
|
|
494
|
+
// two installs in one folder corrupt node_modules, and a build before the
|
|
495
|
+
// install finishes fails for no reason the model could see.
|
|
496
|
+
const alreadyInstalled = await awaitInstall(command, workdir, onOutput);
|
|
497
|
+
if (alreadyInstalled) return alreadyInstalled;
|
|
498
|
+
|
|
358
499
|
// A dev server is backgrounded whether or not the model remembered to ask.
|
|
359
500
|
// Only an explicit `background: false` keeps one in the foreground.
|
|
360
501
|
if (background || (server && background !== false)) {
|