@wrongstack/tools 0.273.1 → 0.275.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/dist/audit.js +57 -19
- package/dist/audit.js.map +1 -1
- package/dist/bash.js +166 -83
- package/dist/bash.js.map +1 -1
- package/dist/builtin.js +669 -189
- package/dist/builtin.js.map +1 -1
- package/dist/circuit-breaker.js +1 -0
- package/dist/circuit-breaker.js.map +1 -1
- package/dist/codebase-index/index.js +24 -16
- package/dist/codebase-index/index.js.map +1 -1
- package/dist/codebase-index/worker.js +24 -16
- package/dist/codebase-index/worker.js.map +1 -1
- package/dist/edit.js +1 -1
- package/dist/edit.js.map +1 -1
- package/dist/exec.js +136 -38
- package/dist/exec.js.map +1 -1
- package/dist/fetch.js +9 -1
- package/dist/fetch.js.map +1 -1
- package/dist/format.js +57 -19
- package/dist/format.js.map +1 -1
- package/dist/glob.js +61 -11
- package/dist/glob.js.map +1 -1
- package/dist/grep.js +92 -39
- package/dist/grep.js.map +1 -1
- package/dist/index.d.ts +37 -1
- package/dist/index.js +680 -200
- package/dist/index.js.map +1 -1
- package/dist/install.js +65 -19
- package/dist/install.js.map +1 -1
- package/dist/lint.js +57 -19
- package/dist/lint.js.map +1 -1
- package/dist/pack.js +667 -188
- package/dist/pack.js.map +1 -1
- package/dist/process-registry.d.ts +30 -0
- package/dist/process-registry.js +57 -19
- package/dist/process-registry.js.map +1 -1
- package/dist/search.js.map +1 -1
- package/dist/test.js +57 -19
- package/dist/test.js.map +1 -1
- package/dist/typecheck.js +57 -19
- package/dist/typecheck.js.map +1 -1
- package/dist/write.js +2 -2
- package/dist/write.js.map +1 -1
- package/package.json +2 -2
package/dist/builtin.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn, execFileSync } from 'node:child_process';
|
|
2
2
|
import * as Core from '@wrongstack/core';
|
|
3
|
-
import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, isPrivateIPv4, isPrivateIPv6, assessCommitSafety, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, toErrorMessage as toErrorMessage$2, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
3
|
+
import { buildChildEnv, getDesignKitLoader, isDesignStack, loadActiveKit, applyTokenOverrides, setActiveKit, recordKitChoice, recordOverrides, setDesignOverrides, materializeTokens, runDesignVerify, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, isPrivateIPv4, isPrivateIPv6, assessCommitSafety, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, toErrorMessage as toErrorMessage$2, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
4
4
|
import * as fs from 'node:fs';
|
|
5
5
|
import { statSync, mkdirSync, createWriteStream } from 'node:fs';
|
|
6
6
|
import * as fs2 from 'node:fs/promises';
|
|
@@ -61,7 +61,7 @@ function createOutputSpool(opts) {
|
|
|
61
61
|
let filePath = null;
|
|
62
62
|
let failed = false;
|
|
63
63
|
let finalized = false;
|
|
64
|
-
const
|
|
64
|
+
const open2 = () => {
|
|
65
65
|
if (stream || failed) return;
|
|
66
66
|
try {
|
|
67
67
|
const dir = toolOutputDir();
|
|
@@ -94,7 +94,7 @@ function createOutputSpool(opts) {
|
|
|
94
94
|
return;
|
|
95
95
|
}
|
|
96
96
|
head += text;
|
|
97
|
-
|
|
97
|
+
open2();
|
|
98
98
|
head = "";
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
@@ -280,6 +280,7 @@ var CircuitBreaker = class {
|
|
|
280
280
|
if (this.state === "open") return;
|
|
281
281
|
this.state = "open";
|
|
282
282
|
this.openedAt = Date.now();
|
|
283
|
+
this.window = [];
|
|
283
284
|
try {
|
|
284
285
|
this.onTrip?.();
|
|
285
286
|
} catch {
|
|
@@ -319,7 +320,7 @@ var SENSITIVE_FLAG_PATTERNS = [
|
|
|
319
320
|
/--(?:token|password|passwd|pwd|secret|api[-_]?key|api[-_]?secret|auth|credential|private[-_]?key|access[-_]?key|github[-_]?token|gh[-_]?token|bearer|jwt|oauth|pin|pincode|passphrase|access[-_]?token)(?:[=\s,][^\s]*)?/gi,
|
|
320
321
|
// -f "value" style short flags
|
|
321
322
|
/(?<!\w)-t(?:\s+|\s*=\s*)[^\s,]+/,
|
|
322
|
-
/(?<!\w)-
|
|
323
|
+
/(?<!\w)-(?:p|password)(?:\s+|\s*=\s*)[^\s,]+/gi,
|
|
323
324
|
// env var–style secrets: TOKEN=x, API_KEY=y, etc.
|
|
324
325
|
/(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
|
|
325
326
|
// Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
|
|
@@ -380,12 +381,35 @@ var ProcessRegistryImpl = class {
|
|
|
380
381
|
register(info) {
|
|
381
382
|
this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });
|
|
382
383
|
}
|
|
384
|
+
_isSafeSignalPid(pid) {
|
|
385
|
+
return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
|
|
386
|
+
}
|
|
387
|
+
_canSignalProcessGroup(p) {
|
|
388
|
+
return os2.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && typeof p.child.pid === "number" && p.child.pid === p.pid;
|
|
389
|
+
}
|
|
390
|
+
_killChildDirect(p, signal) {
|
|
391
|
+
try {
|
|
392
|
+
p.child.kill(signal);
|
|
393
|
+
} catch {
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
_killPosix(p, signal) {
|
|
397
|
+
if (this._canSignalProcessGroup(p)) {
|
|
398
|
+
try {
|
|
399
|
+
process.kill(-p.pid, signal);
|
|
400
|
+
return;
|
|
401
|
+
} catch {
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
this._killChildDirect(p, signal);
|
|
405
|
+
}
|
|
383
406
|
/** Unregister a process by PID. Called on 'close' / 'exit' events. */
|
|
384
407
|
unregister(pid) {
|
|
385
408
|
this.processes.delete(pid);
|
|
386
409
|
}
|
|
387
410
|
/** Get a single process by PID. */
|
|
388
411
|
get(pid) {
|
|
412
|
+
this._pruneStale(pid);
|
|
389
413
|
return this.processes.get(pid);
|
|
390
414
|
}
|
|
391
415
|
/** Get all tracked processes. */
|
|
@@ -549,13 +573,14 @@ var ProcessRegistryImpl = class {
|
|
|
549
573
|
* Returns true if the process was found and kill was attempted.
|
|
550
574
|
*/
|
|
551
575
|
kill(pid, opts = {}) {
|
|
576
|
+
this._pruneStale(pid);
|
|
552
577
|
const p = this.processes.get(pid);
|
|
553
578
|
if (!p) return false;
|
|
554
579
|
if (p.killed) return true;
|
|
555
580
|
if (p.protected) return false;
|
|
556
581
|
const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
|
|
557
|
-
const
|
|
558
|
-
if (
|
|
582
|
+
const isWin4 = os2.platform() === "win32";
|
|
583
|
+
if (isWin4) {
|
|
559
584
|
const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
|
|
560
585
|
if (liveRealChild && killWin32Tree(pid)) {
|
|
561
586
|
const fallback = setTimeout(() => {
|
|
@@ -578,27 +603,12 @@ var ProcessRegistryImpl = class {
|
|
|
578
603
|
}
|
|
579
604
|
try {
|
|
580
605
|
if (force) {
|
|
581
|
-
|
|
582
|
-
process.kill(-pid, "SIGKILL");
|
|
583
|
-
} catch {
|
|
584
|
-
p.child.kill("SIGKILL");
|
|
585
|
-
}
|
|
606
|
+
this._killPosix(p, "SIGKILL");
|
|
586
607
|
} else {
|
|
587
|
-
|
|
588
|
-
process.kill(-pid, "SIGTERM");
|
|
589
|
-
} catch {
|
|
590
|
-
p.child.kill("SIGTERM");
|
|
591
|
-
}
|
|
608
|
+
this._killPosix(p, "SIGTERM");
|
|
592
609
|
const timer = setTimeout(() => {
|
|
593
610
|
if (this.processes.has(pid) && !p.child.killed) {
|
|
594
|
-
|
|
595
|
-
process.kill(-pid, "SIGKILL");
|
|
596
|
-
} catch {
|
|
597
|
-
try {
|
|
598
|
-
p.child.kill("SIGKILL");
|
|
599
|
-
} catch {
|
|
600
|
-
}
|
|
601
|
-
}
|
|
611
|
+
this._killPosix(p, "SIGKILL");
|
|
602
612
|
}
|
|
603
613
|
}, graceMs);
|
|
604
614
|
timer.unref?.();
|
|
@@ -633,6 +643,34 @@ var ProcessRegistryImpl = class {
|
|
|
633
643
|
}
|
|
634
644
|
return killed;
|
|
635
645
|
}
|
|
646
|
+
/**
|
|
647
|
+
* Check whether a tracked process entry is stale — the child has exited
|
|
648
|
+
* (exitCode !== null) AND it's been in the registry long enough that the
|
|
649
|
+
* OS may have reused the PID for a new, unrelated process.
|
|
650
|
+
*
|
|
651
|
+
* P3 #24 (before-release.md): on POSIX, PIDs are reused after process
|
|
652
|
+
* exit. If a tracked process exits but its 'close' event hasn't fired yet
|
|
653
|
+
* (or was missed), the registry still holds the entry. A new process
|
|
654
|
+
* gets the same PID, and PID-based lookups (get, kill, shouldBlockKill)
|
|
655
|
+
* may incorrectly protect or target the wrong process.
|
|
656
|
+
*
|
|
657
|
+
* The 60s threshold is conservative — the OS typically waits much longer
|
|
658
|
+
* before reusing a PID, but we want to clean up before that becomes a risk.
|
|
659
|
+
*/
|
|
660
|
+
_isStaleEntry(entry) {
|
|
661
|
+
return entry.child.exitCode !== null && Date.now() - entry.startedAt > 6e4;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Remove a stale entry for a specific PID before any PID-based lookup.
|
|
665
|
+
* This prevents PID reuse from causing the registry to act on a dead
|
|
666
|
+
* process that has been replaced by a new one with the same PID.
|
|
667
|
+
*/
|
|
668
|
+
_pruneStale(pid) {
|
|
669
|
+
const entry = this.processes.get(pid);
|
|
670
|
+
if (entry && this._isStaleEntry(entry)) {
|
|
671
|
+
this.processes.delete(pid);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
636
674
|
};
|
|
637
675
|
var _registry;
|
|
638
676
|
function getProcessRegistry() {
|
|
@@ -1506,17 +1544,18 @@ function getPersistentProcessRegistry() {
|
|
|
1506
1544
|
}
|
|
1507
1545
|
|
|
1508
1546
|
// src/bash-kill-guard.ts
|
|
1547
|
+
var isWin2 = os2.platform() === "win32";
|
|
1509
1548
|
function extractKillCommand(command) {
|
|
1510
1549
|
const normalized = command.replace(/\s+/g, " ").trim();
|
|
1511
1550
|
const shellCMatch = normalized.match(
|
|
1512
|
-
/^(
|
|
1551
|
+
/^(?:\S+(?:\s+\S+)?)?\s+-c\s+['"](.+?)['"]$/
|
|
1513
1552
|
);
|
|
1514
1553
|
if (shellCMatch?.[1]) {
|
|
1515
1554
|
const inner = shellCMatch[1].trim();
|
|
1516
1555
|
return isKillRelatedCommand(inner) ? inner : null;
|
|
1517
1556
|
}
|
|
1518
1557
|
const shellCUnquoted = normalized.match(
|
|
1519
|
-
/^(
|
|
1558
|
+
/^(?:\S+(?:\s+\S+)?)?\s+-c\s+(kill(?:\s+-[a-zA-Z]+)?(?:\s+\d+)+)$/
|
|
1520
1559
|
);
|
|
1521
1560
|
if (shellCUnquoted?.[1]) {
|
|
1522
1561
|
return shellCUnquoted[1];
|
|
@@ -1525,15 +1564,43 @@ function extractKillCommand(command) {
|
|
|
1525
1564
|
}
|
|
1526
1565
|
function isKillRelatedCommand(cmd) {
|
|
1527
1566
|
const normalized = cmd.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1567
|
+
if (isWin2) {
|
|
1568
|
+
if (/^taskkill\s/i.test(normalized)) return true;
|
|
1569
|
+
if (/^tskill\s/i.test(normalized)) return true;
|
|
1570
|
+
return false;
|
|
1571
|
+
}
|
|
1528
1572
|
if (/^kill(\s|$)/.test(normalized)) return true;
|
|
1529
1573
|
if (/^(pkill|killall|pgrep|skill)\s/.test(normalized)) return true;
|
|
1530
|
-
if (/^taskkill\s/i.test(normalized)) return true;
|
|
1531
|
-
if (/^tskill\s/i.test(normalized)) return true;
|
|
1532
1574
|
if (/^\/proc\/\d+\/(?:kill|fd)/.test(normalized)) return true;
|
|
1533
1575
|
return false;
|
|
1534
1576
|
}
|
|
1535
1577
|
function parseKillCommand(command) {
|
|
1536
1578
|
const normalized = command.replace(/\s+/g, " ").trim();
|
|
1579
|
+
if (isWin2) {
|
|
1580
|
+
const taskkillMatch = normalized.match(/^taskkill\s+(?:\/[a-zA-Z]+\s+)*\/PID\s+(\d+)/i);
|
|
1581
|
+
if (taskkillMatch?.[1]) {
|
|
1582
|
+
const pidStr = taskkillMatch[1];
|
|
1583
|
+
return {
|
|
1584
|
+
pid: parseInt(pidStr, 10),
|
|
1585
|
+
signal: normalized.includes("/F") ? "FORCE" : "TERM",
|
|
1586
|
+
isGroupKill: false,
|
|
1587
|
+
isAllKill: false,
|
|
1588
|
+
originalCommand: command
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
const tskillMatch = normalized.match(/^tskill\s+(\d+)/i);
|
|
1592
|
+
if (tskillMatch?.[1]) {
|
|
1593
|
+
const pidStr = tskillMatch[1];
|
|
1594
|
+
return {
|
|
1595
|
+
pid: parseInt(pidStr, 10),
|
|
1596
|
+
signal: "TERM",
|
|
1597
|
+
isGroupKill: false,
|
|
1598
|
+
isAllKill: false,
|
|
1599
|
+
originalCommand: command
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
return null;
|
|
1603
|
+
}
|
|
1537
1604
|
const simpleMatch = normalized.match(/^kill\s+(?:(-[a-zA-Z]+)\s+)?(\d+|-?\d+)$/);
|
|
1538
1605
|
if (simpleMatch) {
|
|
1539
1606
|
const signal = simpleMatch[1] ?? "-TERM";
|
|
@@ -1577,28 +1644,6 @@ function parseKillCommand(command) {
|
|
|
1577
1644
|
if (pgrepMatch) {
|
|
1578
1645
|
return null;
|
|
1579
1646
|
}
|
|
1580
|
-
const taskkillMatch = normalized.match(/^taskkill\s+(?:\/[a-zA-Z]+\s+)*\/PID\s+(\d+)/i);
|
|
1581
|
-
if (taskkillMatch?.[1]) {
|
|
1582
|
-
const pidStr = taskkillMatch[1];
|
|
1583
|
-
return {
|
|
1584
|
-
pid: parseInt(pidStr, 10),
|
|
1585
|
-
signal: normalized.includes("/F") ? "FORCE" : "TERM",
|
|
1586
|
-
isGroupKill: false,
|
|
1587
|
-
isAllKill: false,
|
|
1588
|
-
originalCommand: command
|
|
1589
|
-
};
|
|
1590
|
-
}
|
|
1591
|
-
const tskillMatch = normalized.match(/^tskill\s+(\d+)/i);
|
|
1592
|
-
if (tskillMatch?.[1]) {
|
|
1593
|
-
const pidStr = tskillMatch[1];
|
|
1594
|
-
return {
|
|
1595
|
-
pid: parseInt(pidStr, 10),
|
|
1596
|
-
signal: "TERM",
|
|
1597
|
-
isGroupKill: false,
|
|
1598
|
-
isAllKill: false,
|
|
1599
|
-
originalCommand: command
|
|
1600
|
-
};
|
|
1601
|
-
}
|
|
1602
1647
|
return null;
|
|
1603
1648
|
}
|
|
1604
1649
|
async function getProtectedEntries() {
|
|
@@ -1675,7 +1720,7 @@ async function checkAndBlockKillCommand(command) {
|
|
|
1675
1720
|
}
|
|
1676
1721
|
return { blocked: false };
|
|
1677
1722
|
}
|
|
1678
|
-
function pickShell(
|
|
1723
|
+
function pickShell(platform4, command, env) {
|
|
1679
1724
|
const override = env.get("WRONGSTACK_SHELL")?.trim().toLowerCase();
|
|
1680
1725
|
if (override === "cmd" || override === "cmd.exe") return "cmd";
|
|
1681
1726
|
if (override === "powershell" || override === "powershell.exe") return "powershell";
|
|
@@ -1729,7 +1774,7 @@ function looksLikePowerShellExtended(command) {
|
|
|
1729
1774
|
}
|
|
1730
1775
|
function wrapPowerShellScript(command) {
|
|
1731
1776
|
const bootstrap = "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8;$ConfirmPreference='None';$WhatIfPreference=$false";
|
|
1732
|
-
return
|
|
1777
|
+
return bootstrap + "\n$ErrorActionPreference='Stop'\n" + command + "\nif ($LASTEXITCODE -is [int]) { exit $LASTEXITCODE }";
|
|
1733
1778
|
}
|
|
1734
1779
|
var PS_VERB_RE = new RegExp(
|
|
1735
1780
|
// Boundaries: start-of-string, whitespace, `;`, `&`, `|`, `(`, `{`, `,`.
|
|
@@ -1874,10 +1919,10 @@ var bashTool = {
|
|
|
1874
1919
|
}));
|
|
1875
1920
|
}
|
|
1876
1921
|
const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
|
|
1877
|
-
const
|
|
1922
|
+
const isWin4 = os2.platform() === "win32";
|
|
1878
1923
|
let plan;
|
|
1879
1924
|
let winShellKind;
|
|
1880
|
-
if (
|
|
1925
|
+
if (isWin4) {
|
|
1881
1926
|
const shell2 = pickShell("win32", input.command, {
|
|
1882
1927
|
get: (k) => process.env[k]
|
|
1883
1928
|
});
|
|
@@ -1906,7 +1951,7 @@ var bashTool = {
|
|
|
1906
1951
|
const shell = plan.bin;
|
|
1907
1952
|
const args = plan.useStdin ? [...plan.argv] : [...plan.argv, input.command];
|
|
1908
1953
|
const env = buildChildEnv(ctx.session?.id);
|
|
1909
|
-
const detached = !
|
|
1954
|
+
const detached = !isWin4;
|
|
1910
1955
|
const startedAt = Date.now();
|
|
1911
1956
|
if (input.background) {
|
|
1912
1957
|
let buf2 = "";
|
|
@@ -1924,7 +1969,7 @@ var bashTool = {
|
|
|
1924
1969
|
// apply: the child gets a hidden console that grandchildren inherit.
|
|
1925
1970
|
// Windows children survive parent exit either way. POSIX keeps
|
|
1926
1971
|
// detached for the process-group kill semantics.
|
|
1927
|
-
detached: !
|
|
1972
|
+
detached: !isWin4,
|
|
1928
1973
|
windowsHide: true
|
|
1929
1974
|
});
|
|
1930
1975
|
if (plan.useStdin) {
|
|
@@ -1942,7 +1987,8 @@ var bashTool = {
|
|
|
1942
1987
|
command: redactCommand(input.command),
|
|
1943
1988
|
startedAt: Date.now(),
|
|
1944
1989
|
sessionId: ctx.session?.id,
|
|
1945
|
-
child: child2
|
|
1990
|
+
child: child2,
|
|
1991
|
+
processGroupLeader: detached && child2.pid === pid2
|
|
1946
1992
|
});
|
|
1947
1993
|
child2.on("close", () => registry.unregister(pid2));
|
|
1948
1994
|
}
|
|
@@ -1983,6 +2029,14 @@ var bashTool = {
|
|
|
1983
2029
|
pid: pid2
|
|
1984
2030
|
}
|
|
1985
2031
|
};
|
|
2032
|
+
ctx.recordSideEffect?.({
|
|
2033
|
+
toolUseId: `bash-bg-${Date.now()}`,
|
|
2034
|
+
toolName: "bash",
|
|
2035
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2036
|
+
input: { command: redactCommand(input.command), background: true },
|
|
2037
|
+
outcome: `launched (pid ${pid2 ?? "unknown"})`,
|
|
2038
|
+
risk: "shell"
|
|
2039
|
+
});
|
|
1986
2040
|
return;
|
|
1987
2041
|
}
|
|
1988
2042
|
const child = spawn(shell, args, {
|
|
@@ -1993,7 +2047,7 @@ var bashTool = {
|
|
|
1993
2047
|
stdio: [plan.useStdin ? "pipe" : "ignore", "pipe", "pipe"],
|
|
1994
2048
|
detached,
|
|
1995
2049
|
windowsHide: true,
|
|
1996
|
-
...
|
|
2050
|
+
...isWin4 ? {} : { signal: opts.signal }
|
|
1997
2051
|
});
|
|
1998
2052
|
if (plan.useStdin) {
|
|
1999
2053
|
try {
|
|
@@ -2010,7 +2064,8 @@ var bashTool = {
|
|
|
2010
2064
|
command: redactCommand(input.command),
|
|
2011
2065
|
startedAt: Date.now(),
|
|
2012
2066
|
sessionId: ctx.session?.id,
|
|
2013
|
-
child
|
|
2067
|
+
child,
|
|
2068
|
+
processGroupLeader: detached && child.pid === pid
|
|
2014
2069
|
});
|
|
2015
2070
|
}
|
|
2016
2071
|
let buf = "";
|
|
@@ -2019,7 +2074,7 @@ var bashTool = {
|
|
|
2019
2074
|
const timers = [];
|
|
2020
2075
|
const spool = createOutputSpool({ tool: "bash", thresholdBytes: MAX_OUTPUT });
|
|
2021
2076
|
function killWithTimeout(child2, timeoutMs2) {
|
|
2022
|
-
if (
|
|
2077
|
+
if (isWin4) {
|
|
2023
2078
|
if (typeof child2.pid === "number" && child2.exitCode === null && killWin32Tree(child2.pid)) {
|
|
2024
2079
|
const fallback = setTimeout(() => {
|
|
2025
2080
|
if (child2.exitCode === null) {
|
|
@@ -2039,34 +2094,22 @@ var bashTool = {
|
|
|
2039
2094
|
}
|
|
2040
2095
|
return;
|
|
2041
2096
|
}
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
} catch {
|
|
2047
|
-
child2.kill("SIGTERM");
|
|
2048
|
-
}
|
|
2049
|
-
} else {
|
|
2097
|
+
if (typeof child2.pid === "number") {
|
|
2098
|
+
registry.kill(child2.pid, { graceMs: timeoutMs2 });
|
|
2099
|
+
} else {
|
|
2100
|
+
try {
|
|
2050
2101
|
child2.kill("SIGTERM");
|
|
2102
|
+
} catch {
|
|
2051
2103
|
}
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
const killTimer = setTimeout(() => {
|
|
2055
|
-
try {
|
|
2056
|
-
if (typeof child2.pid === "number") {
|
|
2057
|
-
try {
|
|
2058
|
-
process.kill(-child2.pid, "SIGKILL");
|
|
2059
|
-
} catch {
|
|
2060
|
-
child2.kill("SIGKILL");
|
|
2061
|
-
}
|
|
2062
|
-
} else {
|
|
2104
|
+
const killTimer = setTimeout(() => {
|
|
2105
|
+
try {
|
|
2063
2106
|
child2.kill("SIGKILL");
|
|
2107
|
+
} catch {
|
|
2064
2108
|
}
|
|
2065
|
-
}
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
killTimer.unref?.();
|
|
2109
|
+
}, timeoutMs2);
|
|
2110
|
+
timers.push(killTimer);
|
|
2111
|
+
killTimer.unref?.();
|
|
2112
|
+
}
|
|
2070
2113
|
}
|
|
2071
2114
|
const timer = setTimeout(() => {
|
|
2072
2115
|
timedOut = true;
|
|
@@ -2075,7 +2118,7 @@ var bashTool = {
|
|
|
2075
2118
|
timers.push(timer);
|
|
2076
2119
|
timer.unref?.();
|
|
2077
2120
|
const onAbort = () => killWithTimeout(child, 2e3);
|
|
2078
|
-
if (
|
|
2121
|
+
if (isWin4) {
|
|
2079
2122
|
if (opts.signal.aborted) onAbort();
|
|
2080
2123
|
else opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
2081
2124
|
}
|
|
@@ -2163,6 +2206,14 @@ ${hint}` : ""),
|
|
|
2163
2206
|
timed_out: timedOut
|
|
2164
2207
|
}
|
|
2165
2208
|
};
|
|
2209
|
+
ctx.recordSideEffect?.({
|
|
2210
|
+
toolUseId: `bash-${Date.now()}`,
|
|
2211
|
+
toolName: "bash",
|
|
2212
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2213
|
+
input: { command: redactCommand(input.command) },
|
|
2214
|
+
outcome: timedOut ? `timed out (exit ${c.code})` : `exit ${c.code}`,
|
|
2215
|
+
risk: "shell"
|
|
2216
|
+
});
|
|
2166
2217
|
return;
|
|
2167
2218
|
}
|
|
2168
2219
|
const now = Date.now();
|
|
@@ -2174,7 +2225,7 @@ ${hint}` : ""),
|
|
|
2174
2225
|
} finally {
|
|
2175
2226
|
for (const t of timers) clearTimeout(t);
|
|
2176
2227
|
spool.finalize();
|
|
2177
|
-
if (
|
|
2228
|
+
if (isWin4) opts.signal.removeEventListener("abort", onAbort);
|
|
2178
2229
|
child.stdout?.off("data", onData);
|
|
2179
2230
|
child.stderr?.off("data", onData);
|
|
2180
2231
|
child.stdout?.destroy();
|
|
@@ -2184,6 +2235,38 @@ ${hint}` : ""),
|
|
|
2184
2235
|
else killWithTimeout(child, 2e3);
|
|
2185
2236
|
}
|
|
2186
2237
|
}
|
|
2238
|
+
},
|
|
2239
|
+
/**
|
|
2240
|
+
* Tool-level teardown fired by `ToolExecutor.runToolCleanup()` when the
|
|
2241
|
+
* tool's run is aborted/timeout'd. The generator's `finally` block above
|
|
2242
|
+
* already force-kills the direct child, but that only runs if the
|
|
2243
|
+
* executor closes the async iterator (via `iter.return()`). When the
|
|
2244
|
+
* executor tears down without iterating — or a re-entrant abort races
|
|
2245
|
+
* with the generator — a bash-spawned process tree can survive in the
|
|
2246
|
+
* ProcessRegistry with `killed === false`, continuing to write files,
|
|
2247
|
+
* consume CPU, or hold inherited stdio pipes open for the rest of the
|
|
2248
|
+
* session.
|
|
2249
|
+
*
|
|
2250
|
+
* This is the defensive layer the executor calls via `tool.cleanup()`
|
|
2251
|
+
* (see `types/tool.ts`): kill every bash-owned process still tracked
|
|
2252
|
+
* for this session that hasn't exited yet. `registry.kill()` already
|
|
2253
|
+
* handles process-group / taskkill tree-kill and the SIGTERM→SIGKILL
|
|
2254
|
+
* grace window, so this just scopes the registry's existing kill path
|
|
2255
|
+
* to "this session's runaway bash children". Idempotent — a process
|
|
2256
|
+
* that already exited is skipped by `kill()` (it returns false), and a
|
|
2257
|
+
* `protected` infrastructure process (dev server the user intentionally
|
|
2258
|
+
* backgrounded) is left alone by design.
|
|
2259
|
+
*/
|
|
2260
|
+
async cleanup(_input, ctx) {
|
|
2261
|
+
const registry = getProcessRegistry();
|
|
2262
|
+
const sessionId = ctx.session?.id;
|
|
2263
|
+
if (!sessionId) return;
|
|
2264
|
+
for (const entry of registry.bySession(sessionId)) {
|
|
2265
|
+
if (entry.name !== "bash") continue;
|
|
2266
|
+
if (entry.child.exitCode !== null) continue;
|
|
2267
|
+
if (entry.protected) continue;
|
|
2268
|
+
registry.kill(entry.pid, { force: true });
|
|
2269
|
+
}
|
|
2187
2270
|
}
|
|
2188
2271
|
};
|
|
2189
2272
|
|
|
@@ -2654,6 +2737,8 @@ var IndexStore = class {
|
|
|
2654
2737
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_kind ON symbols(kind)");
|
|
2655
2738
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_lang ON symbols(lang)");
|
|
2656
2739
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_file ON symbols(file)");
|
|
2740
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_lang_kind ON symbols(lang, kind)");
|
|
2741
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_file_fk ON symbols(file_fk)");
|
|
2657
2742
|
this.db.exec(`
|
|
2658
2743
|
CREATE TABLE IF NOT EXISTS refs (
|
|
2659
2744
|
id INTEGER PRIMARY KEY,
|
|
@@ -3566,9 +3651,9 @@ async function syncGoParse(filePath, content, lang) {
|
|
|
3566
3651
|
}
|
|
3567
3652
|
}
|
|
3568
3653
|
async function parseSymbols3(opts) {
|
|
3569
|
-
const { file, lang } = opts;
|
|
3654
|
+
const { file, content, lang } = opts;
|
|
3570
3655
|
try {
|
|
3571
|
-
return await syncPyParse(file, lang);
|
|
3656
|
+
return await syncPyParse(file, content, lang);
|
|
3572
3657
|
} catch {
|
|
3573
3658
|
return { file, lang, symbols: [], mtimeMs: Date.now() };
|
|
3574
3659
|
}
|
|
@@ -3636,8 +3721,7 @@ syms = []
|
|
|
3636
3721
|
errors = []
|
|
3637
3722
|
|
|
3638
3723
|
try:
|
|
3639
|
-
|
|
3640
|
-
source = f.read()
|
|
3724
|
+
source = sys.stdin.read()
|
|
3641
3725
|
tree = ast.parse(source, filename=sys.argv[1])
|
|
3642
3726
|
except Exception as e:
|
|
3643
3727
|
errors.append(str(e))
|
|
@@ -3777,16 +3861,21 @@ visitor.visit(tree)
|
|
|
3777
3861
|
|
|
3778
3862
|
print(json.dumps([s.to_dict() for s in syms]))
|
|
3779
3863
|
`;
|
|
3780
|
-
|
|
3864
|
+
var _cachedScriptPath = null;
|
|
3865
|
+
async function syncPyParse(filePath, content, lang) {
|
|
3781
3866
|
try {
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3867
|
+
if (!_cachedScriptPath) {
|
|
3868
|
+
const tmpDir = path3.join(os2.tmpdir(), "ws-py-parse");
|
|
3869
|
+
await fs2.mkdir(tmpDir, { recursive: true });
|
|
3870
|
+
_cachedScriptPath = path3.join(tmpDir, "parse.py");
|
|
3871
|
+
await fs2.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
|
|
3872
|
+
}
|
|
3873
|
+
const proc = spawn("python", [_cachedScriptPath, filePath], {
|
|
3787
3874
|
stdio: ["pipe", "pipe", "pipe"],
|
|
3788
3875
|
windowsHide: true
|
|
3789
3876
|
});
|
|
3877
|
+
proc.stdin?.write(content);
|
|
3878
|
+
proc.stdin?.end();
|
|
3790
3879
|
let stdout = "";
|
|
3791
3880
|
proc.stdout?.on("data", (chunk) => {
|
|
3792
3881
|
stdout += chunk.toString();
|
|
@@ -4453,7 +4542,7 @@ async function parseFile(file, content, lang) {
|
|
|
4453
4542
|
case "go":
|
|
4454
4543
|
return parseSymbols2({ file, content, lang: "go" });
|
|
4455
4544
|
case "py":
|
|
4456
|
-
return parseSymbols3({ file, lang: "py" });
|
|
4545
|
+
return parseSymbols3({ file, content, lang: "py" });
|
|
4457
4546
|
case "rs":
|
|
4458
4547
|
return parseSymbols4({ file, content, lang: "rs" });
|
|
4459
4548
|
case "json":
|
|
@@ -4484,10 +4573,12 @@ async function runIndexerWithStore(store, opts) {
|
|
|
4484
4573
|
let symbolsIndexed = 0;
|
|
4485
4574
|
const isGitIgnored = await loadGitignoreMatcher(projectRoot);
|
|
4486
4575
|
let files;
|
|
4576
|
+
let discoveredFiles = null;
|
|
4487
4577
|
if (opts.files && opts.files.length > 0) {
|
|
4488
4578
|
files = opts.files.map((f) => path3.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path3.relative(projectRoot, f).replace(/\\/g, "/"), false));
|
|
4489
4579
|
} else {
|
|
4490
4580
|
files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
|
|
4581
|
+
discoveredFiles = new Set(files);
|
|
4491
4582
|
}
|
|
4492
4583
|
if (langs && langs.length > 0) {
|
|
4493
4584
|
const langSet = new Set(langs);
|
|
@@ -4605,11 +4696,11 @@ async function runIndexerWithStore(store, opts) {
|
|
|
4605
4696
|
filesIndexed++;
|
|
4606
4697
|
}
|
|
4607
4698
|
}
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4699
|
+
if (discoveredFiles) {
|
|
4700
|
+
for (const [file_] of existingMeta) {
|
|
4701
|
+
if (!discoveredFiles.has(file_)) {
|
|
4702
|
+
store.deleteFile(file_);
|
|
4703
|
+
}
|
|
4613
4704
|
}
|
|
4614
4705
|
}
|
|
4615
4706
|
const durationMs = Date.now() - startMs;
|
|
@@ -5126,6 +5217,229 @@ var codebaseStatsTool = {
|
|
|
5126
5217
|
};
|
|
5127
5218
|
}
|
|
5128
5219
|
};
|
|
5220
|
+
function normalizeOverrides(set) {
|
|
5221
|
+
const out = {};
|
|
5222
|
+
if (set && typeof set === "object") {
|
|
5223
|
+
for (const [k, v] of Object.entries(set)) {
|
|
5224
|
+
if (typeof v === "string") out[k] = v;
|
|
5225
|
+
}
|
|
5226
|
+
}
|
|
5227
|
+
return out;
|
|
5228
|
+
}
|
|
5229
|
+
var designTool = {
|
|
5230
|
+
name: "design",
|
|
5231
|
+
category: "Design",
|
|
5232
|
+
description: 'Browse, load, customize, and enforce curated frontend/mobile UI design kits. Use BEFORE writing UI code to commit to one coherent, modern, responsive, dark/light, accessible design. Actions: "list" (menu), "use" (load+pin a kit for a stack), "foundations" (baseline), "set" (override kit colors/tokens), "materialize" (write the tokens to a real theme file \u2014 CSS @theme/OKLCH or native), "verify" (scan UI files for off-palette colors).',
|
|
5233
|
+
usageHint: 'Flow: `design {action:"use", kit:"minimal-clarity", stack:"web"}` \u2192 optionally `design {action:"set", set:{primary:"oklch(62% 0.2 25)"}}` \u2192 `design {action:"materialize"}` to write tokens to disk \u2192 implement against them \u2192 `design {action:"verify"}`.',
|
|
5234
|
+
permission: "auto",
|
|
5235
|
+
mutating: false,
|
|
5236
|
+
capabilities: [],
|
|
5237
|
+
timeoutMs: 15e3,
|
|
5238
|
+
inputSchema: {
|
|
5239
|
+
type: "object",
|
|
5240
|
+
properties: {
|
|
5241
|
+
action: {
|
|
5242
|
+
type: "string",
|
|
5243
|
+
enum: ["list", "use", "foundations", "set", "materialize", "verify"],
|
|
5244
|
+
description: "list = menu; use = load+pin a kit; foundations = baseline; set = override colors/tokens; materialize = write tokens to a theme file; verify = scan UI for off-palette colors. Default: list."
|
|
5245
|
+
},
|
|
5246
|
+
kit: {
|
|
5247
|
+
type: "string",
|
|
5248
|
+
description: 'Kit id (required for "use"), e.g. "minimal-clarity", "neo-brutalist".'
|
|
5249
|
+
},
|
|
5250
|
+
stack: {
|
|
5251
|
+
type: "string",
|
|
5252
|
+
enum: ["web", "react-native", "flutter", "swiftui", "compose"],
|
|
5253
|
+
description: "Target stack \u2014 narrows guidance + materialize format. Default: web."
|
|
5254
|
+
},
|
|
5255
|
+
set: {
|
|
5256
|
+
type: "object",
|
|
5257
|
+
description: 'Token overrides for "set"/"use": { "primary": "oklch(\u2026)", "dark.bg": "#111" }. Bare key = both themes; "light."/"dark." prefix = that theme only. Empty value clears an override.',
|
|
5258
|
+
additionalProperties: { type: "string" }
|
|
5259
|
+
},
|
|
5260
|
+
out: {
|
|
5261
|
+
type: "string",
|
|
5262
|
+
description: "Materialize output path (project-relative). Defaults to a per-stack convention."
|
|
5263
|
+
},
|
|
5264
|
+
force: {
|
|
5265
|
+
type: "boolean",
|
|
5266
|
+
description: "Materialize: overwrite an existing file (default false \u2014 refuses to clobber)."
|
|
5267
|
+
},
|
|
5268
|
+
files: {
|
|
5269
|
+
type: "array",
|
|
5270
|
+
items: { type: "string" },
|
|
5271
|
+
description: "Verify: explicit project-relative files to scan. Default: a bounded UI-file walk."
|
|
5272
|
+
}
|
|
5273
|
+
},
|
|
5274
|
+
required: []
|
|
5275
|
+
},
|
|
5276
|
+
async execute(input, ctx) {
|
|
5277
|
+
const loader = getDesignKitLoader(ctx.projectRoot);
|
|
5278
|
+
const action = input.action ?? "list";
|
|
5279
|
+
const stack = input.stack && isDesignStack(input.stack) ? input.stack : void 0;
|
|
5280
|
+
if (action === "foundations") {
|
|
5281
|
+
const text = await loader.foundationsText(stack);
|
|
5282
|
+
return { action, stack, output: text || "No foundations document is installed." };
|
|
5283
|
+
}
|
|
5284
|
+
if (action === "use") {
|
|
5285
|
+
const kitId = input.kit?.trim();
|
|
5286
|
+
if (!kitId) {
|
|
5287
|
+
const menu2 = await loader.menuText();
|
|
5288
|
+
return { action, output: `No kit id provided.
|
|
5289
|
+
|
|
5290
|
+
${menu2}` };
|
|
5291
|
+
}
|
|
5292
|
+
const manifest = await loader.find(kitId);
|
|
5293
|
+
if (!manifest) {
|
|
5294
|
+
const menu2 = await loader.menuText();
|
|
5295
|
+
return { action, kit: kitId, output: `Kit "${kitId}" not found.
|
|
5296
|
+
|
|
5297
|
+
${menu2}` };
|
|
5298
|
+
}
|
|
5299
|
+
const resolvedStack = stack ?? manifest.stacks[0] ?? "web";
|
|
5300
|
+
const body = await loader.readBody(manifest.id, resolvedStack);
|
|
5301
|
+
const rawTokens = await loader.readTokens(manifest.id);
|
|
5302
|
+
const persisted = await loadActiveKit(ctx.projectRoot);
|
|
5303
|
+
const keepOverrides = persisted?.kit === manifest.id ? persisted.overrides ?? {} : {};
|
|
5304
|
+
const overrides = { ...keepOverrides, ...normalizeOverrides(input.set) };
|
|
5305
|
+
const tokens = rawTokens ? applyTokenOverrides(rawTokens, overrides) : rawTokens;
|
|
5306
|
+
setActiveKit(ctx, manifest.id, resolvedStack, overrides);
|
|
5307
|
+
await recordKitChoice(
|
|
5308
|
+
ctx.projectRoot,
|
|
5309
|
+
manifest.id,
|
|
5310
|
+
resolvedStack,
|
|
5311
|
+
"design-tool",
|
|
5312
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
5313
|
+
Object.keys(overrides).length ? overrides : void 0
|
|
5314
|
+
);
|
|
5315
|
+
const ovLine = Object.keys(overrides).length ? `
|
|
5316
|
+
Active color overrides: ${Object.entries(overrides).map(([k, v]) => `${k}=${v}`).join(", ")}
|
|
5317
|
+
` : "";
|
|
5318
|
+
const header = `# Active design kit: ${manifest.name} (${manifest.id}) \u2014 stack: ${resolvedStack}
|
|
5319
|
+
${manifest.aesthetic}
|
|
5320
|
+
${ovLine}
|
|
5321
|
+
Implement the UI faithfully to this spec. Keep light/dark, responsive, and WCAG AA.
|
|
5322
|
+
`;
|
|
5323
|
+
const tokenBlock = tokens ? `
|
|
5324
|
+
## Token snapshot (overrides applied)
|
|
5325
|
+
\`\`\`json
|
|
5326
|
+
${JSON.stringify(tokens, null, 2)}
|
|
5327
|
+
\`\`\`
|
|
5328
|
+
Tip: run \`design {action:"materialize"}\` to write these tokens to a real theme file.
|
|
5329
|
+
` : "";
|
|
5330
|
+
return {
|
|
5331
|
+
action,
|
|
5332
|
+
kit: manifest.id,
|
|
5333
|
+
stack: resolvedStack,
|
|
5334
|
+
output: `${header}${tokenBlock}
|
|
5335
|
+
${body}`
|
|
5336
|
+
};
|
|
5337
|
+
}
|
|
5338
|
+
if (action === "set") {
|
|
5339
|
+
const patch = normalizeOverrides(input.set);
|
|
5340
|
+
if (Object.keys(patch).length === 0) {
|
|
5341
|
+
return { action, output: 'No overrides given. Pass set:{ "primary": "oklch(\u2026)" }.' };
|
|
5342
|
+
}
|
|
5343
|
+
const merged = await recordOverrides(ctx.projectRoot, patch, (/* @__PURE__ */ new Date()).toISOString());
|
|
5344
|
+
if (!merged) {
|
|
5345
|
+
return {
|
|
5346
|
+
action,
|
|
5347
|
+
output: 'No active kit. Pick one first: `design {action:"use", kit:"<id>"}`.'
|
|
5348
|
+
};
|
|
5349
|
+
}
|
|
5350
|
+
setDesignOverrides(ctx, merged);
|
|
5351
|
+
return {
|
|
5352
|
+
action,
|
|
5353
|
+
output: `Overrides updated. Active overrides: ${Object.entries(merged).map(([k, v]) => `${k}=${v}`).join(", ")}
|
|
5354
|
+
These win over kit tokens. Run \`design {action:"materialize"}\` to write them to a theme file.`
|
|
5355
|
+
};
|
|
5356
|
+
}
|
|
5357
|
+
if (action === "materialize") {
|
|
5358
|
+
const active = await loadActiveKit(ctx.projectRoot);
|
|
5359
|
+
if (!active) {
|
|
5360
|
+
return {
|
|
5361
|
+
action,
|
|
5362
|
+
output: 'No active kit. Pick one first: `design {action:"use", kit:"<id>"}`.'
|
|
5363
|
+
};
|
|
5364
|
+
}
|
|
5365
|
+
const resolvedStack = stack ?? (active.stack && isDesignStack(active.stack) ? active.stack : "web");
|
|
5366
|
+
const rawTokens = await loader.readTokens(active.kit);
|
|
5367
|
+
if (!rawTokens) {
|
|
5368
|
+
return { action, kit: active.kit, output: `Kit "${active.kit}" has no tokens.json.` };
|
|
5369
|
+
}
|
|
5370
|
+
const tokens = applyTokenOverrides(rawTokens, active.overrides);
|
|
5371
|
+
const result = materializeTokens({
|
|
5372
|
+
tokens,
|
|
5373
|
+
stack: resolvedStack,
|
|
5374
|
+
kitId: active.kit,
|
|
5375
|
+
outPath: input.out
|
|
5376
|
+
});
|
|
5377
|
+
const abs = path3.join(ctx.projectRoot, result.path);
|
|
5378
|
+
let exists = false;
|
|
5379
|
+
try {
|
|
5380
|
+
await fs2.access(abs);
|
|
5381
|
+
exists = true;
|
|
5382
|
+
} catch {
|
|
5383
|
+
}
|
|
5384
|
+
if (exists && !input.force) {
|
|
5385
|
+
return {
|
|
5386
|
+
action,
|
|
5387
|
+
kit: active.kit,
|
|
5388
|
+
stack: resolvedStack,
|
|
5389
|
+
path: result.path,
|
|
5390
|
+
output: `${result.path} already exists. Re-run with force:true to overwrite, or write this ${result.format} yourself:
|
|
5391
|
+
|
|
5392
|
+
\`\`\`
|
|
5393
|
+
${result.content}
|
|
5394
|
+
\`\`\``
|
|
5395
|
+
};
|
|
5396
|
+
}
|
|
5397
|
+
await fs2.mkdir(path3.dirname(abs), { recursive: true });
|
|
5398
|
+
await fs2.writeFile(abs, result.content);
|
|
5399
|
+
return {
|
|
5400
|
+
action,
|
|
5401
|
+
kit: active.kit,
|
|
5402
|
+
stack: resolvedStack,
|
|
5403
|
+
path: result.path,
|
|
5404
|
+
output: `Wrote ${result.format} to ${result.path}. Import these tokens in your UI so the kit palette is the source of truth. ${exists ? "(overwrote existing file)" : ""}`
|
|
5405
|
+
};
|
|
5406
|
+
}
|
|
5407
|
+
if (action === "verify") {
|
|
5408
|
+
const active = await loadActiveKit(ctx.projectRoot);
|
|
5409
|
+
if (!active) {
|
|
5410
|
+
return {
|
|
5411
|
+
action,
|
|
5412
|
+
output: 'No active kit to verify against. Pick one: `design {action:"use", kit:"<id>"}`.'
|
|
5413
|
+
};
|
|
5414
|
+
}
|
|
5415
|
+
const rawTokens = await loader.readTokens(active.kit);
|
|
5416
|
+
if (!rawTokens) {
|
|
5417
|
+
return { action, kit: active.kit, output: `Kit "${active.kit}" has no tokens.json.` };
|
|
5418
|
+
}
|
|
5419
|
+
const tokens = applyTokenOverrides(rawTokens, active.overrides);
|
|
5420
|
+
const report = await runDesignVerify(ctx.projectRoot, tokens, input.files);
|
|
5421
|
+
const pct = Math.round(report.score * 100);
|
|
5422
|
+
const top = report.violations.slice(0, 25).map((v) => ` ${v.file}:${v.line} \u2014 ${v.reason}: ${v.snippet}`).join("\n");
|
|
5423
|
+
const summary = `Adherence: ${pct}% on-palette across ${report.filesScanned} file(s). ${report.violations.length} violation(s).` + (report.violations.length ? `
|
|
5424
|
+
${top}${report.violations.length > 25 ? `
|
|
5425
|
+
\u2026and ${report.violations.length - 25} more` : ""}
|
|
5426
|
+
|
|
5427
|
+
Replace off-palette colors with kit tokens (or the materialized CSS vars / token utilities).` : "\nNo off-palette colors found \u2014 UI adheres to the kit palette.");
|
|
5428
|
+
return {
|
|
5429
|
+
action,
|
|
5430
|
+
kit: active.kit,
|
|
5431
|
+
output: summary,
|
|
5432
|
+
score: report.score,
|
|
5433
|
+
violations: report.violations.length
|
|
5434
|
+
};
|
|
5435
|
+
}
|
|
5436
|
+
const menu = await loader.menuText();
|
|
5437
|
+
return {
|
|
5438
|
+
action: "list",
|
|
5439
|
+
output: (menu || "No design kits are installed.") + '\n\nLoad one with `design {action:"use", kit:"<id>", stack:"<stack>"}`.'
|
|
5440
|
+
};
|
|
5441
|
+
}
|
|
5442
|
+
};
|
|
5129
5443
|
var diffTool = {
|
|
5130
5444
|
name: "diff",
|
|
5131
5445
|
category: "Filesystem",
|
|
@@ -5500,7 +5814,7 @@ var editTool = {
|
|
|
5500
5814
|
const newFile = toStyle(newFileLf, style);
|
|
5501
5815
|
await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
|
|
5502
5816
|
const written = await fs2.stat(absPath);
|
|
5503
|
-
ctx.recordRead(absPath, written.mtimeMs);
|
|
5817
|
+
ctx.recordRead(absPath, written.mtimeMs, "write");
|
|
5504
5818
|
ctx.session.recordFileChange({
|
|
5505
5819
|
path: absPath,
|
|
5506
5820
|
action: "modified",
|
|
@@ -5543,7 +5857,7 @@ function findSimilarity(haystack, needle) {
|
|
|
5543
5857
|
}
|
|
5544
5858
|
return line;
|
|
5545
5859
|
}
|
|
5546
|
-
var
|
|
5860
|
+
var isWin3 = process.platform === "win32";
|
|
5547
5861
|
var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
5548
5862
|
// JS / TS toolchain
|
|
5549
5863
|
"node",
|
|
@@ -5553,12 +5867,25 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
5553
5867
|
"npx",
|
|
5554
5868
|
"bun",
|
|
5555
5869
|
"deno",
|
|
5870
|
+
"corepack",
|
|
5556
5871
|
"tsc",
|
|
5872
|
+
"tsx",
|
|
5873
|
+
"ts-node",
|
|
5874
|
+
"vite",
|
|
5557
5875
|
"vitest",
|
|
5558
5876
|
"jest",
|
|
5559
5877
|
"biome",
|
|
5560
5878
|
"eslint",
|
|
5561
5879
|
"prettier",
|
|
5880
|
+
"turbo",
|
|
5881
|
+
"nx",
|
|
5882
|
+
"webpack",
|
|
5883
|
+
"rollup",
|
|
5884
|
+
"parcel",
|
|
5885
|
+
"next",
|
|
5886
|
+
"astro",
|
|
5887
|
+
"playwright",
|
|
5888
|
+
"cypress",
|
|
5562
5889
|
// version control
|
|
5563
5890
|
"git",
|
|
5564
5891
|
// Rust
|
|
@@ -5571,10 +5898,22 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
5571
5898
|
"python3",
|
|
5572
5899
|
"pip",
|
|
5573
5900
|
"pip3",
|
|
5901
|
+
"pytest",
|
|
5902
|
+
"ruff",
|
|
5903
|
+
"mypy",
|
|
5904
|
+
"uv",
|
|
5905
|
+
"uvx",
|
|
5906
|
+
"poetry",
|
|
5907
|
+
"hatch",
|
|
5908
|
+
"tox",
|
|
5574
5909
|
// Ruby
|
|
5575
5910
|
"ruby",
|
|
5576
5911
|
"gem",
|
|
5577
5912
|
"bundle",
|
|
5913
|
+
// PHP
|
|
5914
|
+
"php",
|
|
5915
|
+
"composer",
|
|
5916
|
+
"phpunit",
|
|
5578
5917
|
// JVM
|
|
5579
5918
|
"java",
|
|
5580
5919
|
"javac",
|
|
@@ -5586,18 +5925,25 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
5586
5925
|
// C / C++ / native build
|
|
5587
5926
|
"make",
|
|
5588
5927
|
"cmake",
|
|
5589
|
-
// containers / orchestration
|
|
5928
|
+
// containers / orchestration
|
|
5590
5929
|
"docker",
|
|
5930
|
+
"podman",
|
|
5591
5931
|
"kubectl",
|
|
5592
5932
|
// common POSIX file/text utilities
|
|
5933
|
+
"pwd",
|
|
5593
5934
|
"ls",
|
|
5594
5935
|
"cat",
|
|
5595
5936
|
"head",
|
|
5596
5937
|
"tail",
|
|
5597
5938
|
"wc",
|
|
5598
5939
|
"grep",
|
|
5940
|
+
"rg",
|
|
5599
5941
|
"find",
|
|
5600
5942
|
"echo",
|
|
5943
|
+
"sort",
|
|
5944
|
+
"uniq",
|
|
5945
|
+
"sed",
|
|
5946
|
+
"awk",
|
|
5601
5947
|
"mkdir",
|
|
5602
5948
|
"cp",
|
|
5603
5949
|
"mv",
|
|
@@ -5613,8 +5959,7 @@ var MAX_ARGS = 20;
|
|
|
5613
5959
|
var MAX_OUTPUT2 = 2e5;
|
|
5614
5960
|
var DEFAULT_TIMEOUT_MS2 = 3e4;
|
|
5615
5961
|
var BLOCKED_ARG_PATTERNS = {
|
|
5616
|
-
|
|
5617
|
-
python: [/-c$/, /^--command$/, /^-m$/, /^--module$/],
|
|
5962
|
+
python: [],
|
|
5618
5963
|
// git --exec=<cmd> runs arbitrary commands via upload-pack/receive-pack;
|
|
5619
5964
|
// -C <dir> changes working directory, bypassing cwd sandbox;
|
|
5620
5965
|
// -c/--config <k>=<v> injects config that runs commands
|
|
@@ -5630,15 +5975,10 @@ var BLOCKED_ARG_PATTERNS = {
|
|
|
5630
5975
|
/^--config=/,
|
|
5631
5976
|
/^--config-env=/
|
|
5632
5977
|
],
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
// bun --preload is similar to node --require
|
|
5638
|
-
bun: [/^--preload$/, /^run$/, /^bunx$/, /^create$/, /^init$/],
|
|
5639
|
-
// docker build/run can create containers with host access;
|
|
5640
|
-
// only allow read-only commands (ps, images, version)
|
|
5641
|
-
docker: [/^build$/, /^run$/, /^exec$/, /^push$/, /^pull$/],
|
|
5978
|
+
node: [],
|
|
5979
|
+
go: [],
|
|
5980
|
+
bun: [],
|
|
5981
|
+
docker: [],
|
|
5642
5982
|
// find -exec/-ok/-execdir execute arbitrary commands
|
|
5643
5983
|
find: [/^-exec$/, /^-exec;$/, /^-ok$/, /^-ok;$/, /^-execdir$/, /^-execdir;$/, /^-exec=/, /^-ok=/, /^-execdir=/],
|
|
5644
5984
|
// rm -rf / is catastrophic — block absolute paths, home, dot-dirs,
|
|
@@ -5646,15 +5986,49 @@ var BLOCKED_ARG_PATTERNS = {
|
|
|
5646
5986
|
// `rm -rf ./src/*` expands to project files; `rm -rf ../../` escapes upward;
|
|
5647
5987
|
// `rm -rf /*` targets the filesystem root. All are blocked.
|
|
5648
5988
|
rm: [/^\//, /^~\//, /^~$/, /^\.$/, /^\.\.$/, /\*$/, /\/$/, /\/\*$/, /\.\//],
|
|
5649
|
-
// npm
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
pnpm: [
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5989
|
+
// npm/pnpm subcommands are checked separately below. Matching every arg here
|
|
5990
|
+
// over-blocked normal dev flows such as `pnpm vitest run ...`.
|
|
5991
|
+
npm: [],
|
|
5992
|
+
pnpm: [],
|
|
5993
|
+
npx: []
|
|
5994
|
+
};
|
|
5995
|
+
var BLOCKED_SUBCOMMANDS = {
|
|
5996
|
+
docker: /* @__PURE__ */ new Set(["push"]),
|
|
5997
|
+
podman: /* @__PURE__ */ new Set(["push"]),
|
|
5998
|
+
npm: /* @__PURE__ */ new Set(["publish", "deploy"]),
|
|
5999
|
+
pnpm: /* @__PURE__ */ new Set(["publish", "deploy"]),
|
|
6000
|
+
yarn: /* @__PURE__ */ new Set(["publish"])
|
|
5656
6001
|
};
|
|
6002
|
+
var BLOCKED_SUBCOMMAND_SEQUENCES = {
|
|
6003
|
+
yarn: [["npm", "publish"]]
|
|
6004
|
+
};
|
|
6005
|
+
function firstSubcommand(args) {
|
|
6006
|
+
for (const arg of args) {
|
|
6007
|
+
if (arg === "--") return null;
|
|
6008
|
+
if (!arg.startsWith("-")) return arg;
|
|
6009
|
+
}
|
|
6010
|
+
return null;
|
|
6011
|
+
}
|
|
6012
|
+
function subcommandArgs(args) {
|
|
6013
|
+
const out = [];
|
|
6014
|
+
for (const arg of args) {
|
|
6015
|
+
if (arg === "--") break;
|
|
6016
|
+
if (!arg.startsWith("-")) out.push(arg);
|
|
6017
|
+
}
|
|
6018
|
+
return out;
|
|
6019
|
+
}
|
|
5657
6020
|
function validateArgs(cmd, args) {
|
|
6021
|
+
const blockedSubcommands = BLOCKED_SUBCOMMANDS[cmd];
|
|
6022
|
+
const subcommand = firstSubcommand(args);
|
|
6023
|
+
if (blockedSubcommands && subcommand && blockedSubcommands.has(subcommand)) {
|
|
6024
|
+
return `Blocked subcommand "${subcommand}" for command "${cmd}"`;
|
|
6025
|
+
}
|
|
6026
|
+
const blockedSequences = BLOCKED_SUBCOMMAND_SEQUENCES[cmd];
|
|
6027
|
+
if (blockedSequences) {
|
|
6028
|
+
const actual = subcommandArgs(args);
|
|
6029
|
+
const blocked2 = blockedSequences.find((seq) => seq.every((part, idx) => actual[idx] === part));
|
|
6030
|
+
if (blocked2) return `Blocked subcommand "${blocked2.join(" ")}" for command "${cmd}"`;
|
|
6031
|
+
}
|
|
5658
6032
|
const blocked = BLOCKED_ARG_PATTERNS[cmd];
|
|
5659
6033
|
if (!blocked) return null;
|
|
5660
6034
|
for (const arg of args) {
|
|
@@ -5781,7 +6155,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
5781
6155
|
const startedAt = Date.now();
|
|
5782
6156
|
const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
|
|
5783
6157
|
const resolved = resolveWin32Command(cmd);
|
|
5784
|
-
const needsShell =
|
|
6158
|
+
const needsShell = isWin3 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
5785
6159
|
const spawnCmd = needsShell ? cmd : resolved;
|
|
5786
6160
|
if (needsShell) assertSafeWin32ShellArgs(args);
|
|
5787
6161
|
let child;
|
|
@@ -5791,7 +6165,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
5791
6165
|
env: buildChildEnv(sessionId),
|
|
5792
6166
|
stdio: ["ignore", "pipe", "pipe"],
|
|
5793
6167
|
windowsHide: true,
|
|
5794
|
-
...
|
|
6168
|
+
...isWin3 ? {} : { signal },
|
|
5795
6169
|
...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
|
|
5796
6170
|
});
|
|
5797
6171
|
} catch (err) {
|
|
@@ -5811,7 +6185,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
5811
6185
|
const isAbort = err && err.code === "ABORT_ERR";
|
|
5812
6186
|
const stderrText = isAbort ? `Aborted: ${err.message}` : err.message;
|
|
5813
6187
|
clearTimeout(timer);
|
|
5814
|
-
if (
|
|
6188
|
+
if (isWin3) signal.removeEventListener("abort", onAbort);
|
|
5815
6189
|
if (typeof pid === "number") registry.unregister(pid);
|
|
5816
6190
|
registry.afterCall(Date.now() - startedAt, true);
|
|
5817
6191
|
spool.finalize();
|
|
@@ -5841,7 +6215,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
5841
6215
|
if (typeof pid === "number") registry.kill(pid, { force: true });
|
|
5842
6216
|
else child.kill("SIGTERM");
|
|
5843
6217
|
};
|
|
5844
|
-
if (
|
|
6218
|
+
if (isWin3) {
|
|
5845
6219
|
if (signal.aborted) onAbort();
|
|
5846
6220
|
else signal.addEventListener("abort", onAbort, { once: true });
|
|
5847
6221
|
}
|
|
@@ -5857,7 +6231,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
5857
6231
|
});
|
|
5858
6232
|
child.on("close", (code) => {
|
|
5859
6233
|
clearTimeout(timer);
|
|
5860
|
-
if (
|
|
6234
|
+
if (isWin3) signal.removeEventListener("abort", onAbort);
|
|
5861
6235
|
if (typeof pid === "number") registry.unregister(pid);
|
|
5862
6236
|
const durationMs = Date.now() - startedAt;
|
|
5863
6237
|
const exitCode = killed ? 124 : code ?? 1;
|
|
@@ -6021,7 +6395,7 @@ var fetchTool = {
|
|
|
6021
6395
|
if (!final) throw new Error("fetch: stream ended without final event");
|
|
6022
6396
|
return final;
|
|
6023
6397
|
},
|
|
6024
|
-
async *executeStream(input,
|
|
6398
|
+
async *executeStream(input, ctx, opts) {
|
|
6025
6399
|
if (!input?.url) throw new Error("fetch: url is required");
|
|
6026
6400
|
const u = new URL(input.url);
|
|
6027
6401
|
if (u.protocol !== "https:" && u.protocol !== "http:") {
|
|
@@ -6093,6 +6467,14 @@ var fetchTool = {
|
|
|
6093
6467
|
url: res.url
|
|
6094
6468
|
}
|
|
6095
6469
|
};
|
|
6470
|
+
ctx.recordSideEffect?.({
|
|
6471
|
+
toolUseId: `fetch-${Date.now()}`,
|
|
6472
|
+
toolName: "fetch",
|
|
6473
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6474
|
+
input: { url: input.url, format: input.format },
|
|
6475
|
+
outcome: `HTTP ${res.status} (${ct})`,
|
|
6476
|
+
risk: "network"
|
|
6477
|
+
});
|
|
6096
6478
|
} finally {
|
|
6097
6479
|
clearTimeout(timer);
|
|
6098
6480
|
}
|
|
@@ -6530,7 +6912,27 @@ function runGit2(args, cwd, signal) {
|
|
|
6530
6912
|
});
|
|
6531
6913
|
});
|
|
6532
6914
|
}
|
|
6915
|
+
|
|
6916
|
+
// src/_concurrency.ts
|
|
6917
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
6918
|
+
if (items.length === 0) return [];
|
|
6919
|
+
const effectiveLimit = Math.max(1, Math.min(limit | 0, items.length));
|
|
6920
|
+
const results = new Array(items.length);
|
|
6921
|
+
let nextIndex = 0;
|
|
6922
|
+
const worker2 = async () => {
|
|
6923
|
+
while (true) {
|
|
6924
|
+
const i = nextIndex++;
|
|
6925
|
+
if (i >= items.length) return;
|
|
6926
|
+
results[i] = await fn(items[i]);
|
|
6927
|
+
}
|
|
6928
|
+
};
|
|
6929
|
+
await Promise.all(Array.from({ length: effectiveLimit }, worker2));
|
|
6930
|
+
return results;
|
|
6931
|
+
}
|
|
6932
|
+
|
|
6933
|
+
// src/glob.ts
|
|
6533
6934
|
var DEFAULT_IGNORE2 = ["node_modules", ".git", "dist", "build", ".next", "coverage", ".turbo"];
|
|
6935
|
+
var WALK_CONCURRENCY = 16;
|
|
6534
6936
|
var globTool = {
|
|
6535
6937
|
name: "glob",
|
|
6536
6938
|
category: "Filesystem",
|
|
@@ -6568,6 +6970,22 @@ var globTool = {
|
|
|
6568
6970
|
const re = compileGlob(input.pattern);
|
|
6569
6971
|
const results = [];
|
|
6570
6972
|
let truncated = false;
|
|
6973
|
+
const pushResult = async (full) => {
|
|
6974
|
+
if (truncated || results.length >= limit) {
|
|
6975
|
+
truncated = true;
|
|
6976
|
+
return;
|
|
6977
|
+
}
|
|
6978
|
+
try {
|
|
6979
|
+
const st = await fs2.stat(full);
|
|
6980
|
+
if (truncated || results.length >= limit) {
|
|
6981
|
+
truncated = true;
|
|
6982
|
+
return;
|
|
6983
|
+
}
|
|
6984
|
+
results.push({ rel: full, mtime: st.mtimeMs });
|
|
6985
|
+
if (results.length >= limit) truncated = true;
|
|
6986
|
+
} catch {
|
|
6987
|
+
}
|
|
6988
|
+
};
|
|
6571
6989
|
const walk = async (dir, relPrefix) => {
|
|
6572
6990
|
if (results.length >= limit) {
|
|
6573
6991
|
truncated = true;
|
|
@@ -6579,6 +6997,8 @@ var globTool = {
|
|
|
6579
6997
|
} catch {
|
|
6580
6998
|
return;
|
|
6581
6999
|
}
|
|
7000
|
+
const subdirs = [];
|
|
7001
|
+
const matchedFiles = [];
|
|
6582
7002
|
for (const e of entries) {
|
|
6583
7003
|
const name = e.name;
|
|
6584
7004
|
if (DEFAULT_IGNORE2.includes(name)) continue;
|
|
@@ -6586,22 +7006,36 @@ var globTool = {
|
|
|
6586
7006
|
const rel = relPrefix ? `${relPrefix}/${name}` : name;
|
|
6587
7007
|
const full = path3.join(dir, name);
|
|
6588
7008
|
if (e.isDirectory()) {
|
|
6589
|
-
|
|
6590
|
-
if (truncated) return;
|
|
7009
|
+
subdirs.push({ full, rel });
|
|
6591
7010
|
} else if (e.isFile()) {
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
7011
|
+
re.lastIndex = 0;
|
|
7012
|
+
const relMatch = re.test(rel);
|
|
7013
|
+
re.lastIndex = 0;
|
|
7014
|
+
const nameMatch = re.test(name);
|
|
7015
|
+
if (relMatch || nameMatch) {
|
|
7016
|
+
matchedFiles.push(full);
|
|
7017
|
+
}
|
|
7018
|
+
} else if (e.isSymbolicLink()) {
|
|
7019
|
+
try {
|
|
7020
|
+
const st = await fs2.stat(full);
|
|
7021
|
+
if (st.isDirectory()) {
|
|
7022
|
+
subdirs.push({ full, rel });
|
|
7023
|
+
} else if (st.isFile()) {
|
|
7024
|
+
re.lastIndex = 0;
|
|
7025
|
+
const relMatch = re.test(rel);
|
|
7026
|
+
re.lastIndex = 0;
|
|
7027
|
+
const nameMatch = re.test(name);
|
|
7028
|
+
if (relMatch || nameMatch) matchedFiles.push(full);
|
|
6601
7029
|
}
|
|
7030
|
+
} catch {
|
|
6602
7031
|
}
|
|
6603
7032
|
}
|
|
7033
|
+
if (truncated) return;
|
|
6604
7034
|
}
|
|
7035
|
+
await mapWithConcurrency(matchedFiles, WALK_CONCURRENCY, pushResult);
|
|
7036
|
+
if (truncated) return;
|
|
7037
|
+
const remainingSubdirs = truncated ? [] : subdirs;
|
|
7038
|
+
await mapWithConcurrency(remainingSubdirs, WALK_CONCURRENCY, ({ full, rel }) => walk(full, rel));
|
|
6605
7039
|
};
|
|
6606
7040
|
await walk(base, "");
|
|
6607
7041
|
results.sort((a, b) => b.mtime - a.mtime);
|
|
@@ -6665,6 +7099,8 @@ function capSubject(line) {
|
|
|
6665
7099
|
// src/grep.ts
|
|
6666
7100
|
var DEFAULT_IGNORE3 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
|
|
6667
7101
|
var NATIVE_SCAN_CONCURRENCY = 32;
|
|
7102
|
+
var NATIVE_READ_CHUNK_BYTES = 64 * 1024;
|
|
7103
|
+
var NATIVE_MAX_FILE_BYTES = 1e6;
|
|
6668
7104
|
var grepTool = {
|
|
6669
7105
|
name: "grep",
|
|
6670
7106
|
category: "Search",
|
|
@@ -6889,7 +7325,8 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
6889
7325
|
const re = compiled.regex;
|
|
6890
7326
|
const globRe = input.glob ? compileGlob(input.glob) : null;
|
|
6891
7327
|
const matches = [];
|
|
6892
|
-
const
|
|
7328
|
+
const countOnlyFirstHit = mode === "count" && limit === 1;
|
|
7329
|
+
const maxBytes = mode === "content" ? NATIVE_MAX_FILE_BYTES : Math.min(NATIVE_MAX_FILE_BYTES, 256 * 1024);
|
|
6893
7330
|
let total = 0;
|
|
6894
7331
|
let stopped = false;
|
|
6895
7332
|
const scanFile = async (full, name) => {
|
|
@@ -6898,34 +7335,79 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
6898
7335
|
if (globRe) globRe.lastIndex = 0;
|
|
6899
7336
|
try {
|
|
6900
7337
|
const stat11 = await fs2.stat(full);
|
|
6901
|
-
if (stat11.size >
|
|
6902
|
-
const
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
const
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
7338
|
+
if (!stat11.isFile() || stat11.size > maxBytes || stopped || signal.aborted) return;
|
|
7339
|
+
const file = await fs2.open(full, "r");
|
|
7340
|
+
try {
|
|
7341
|
+
let bytesReadTotal = 0;
|
|
7342
|
+
let lineNumber = 0;
|
|
7343
|
+
let fileHits = 0;
|
|
7344
|
+
let leftover = "";
|
|
7345
|
+
let binaryChecked = false;
|
|
7346
|
+
const buffer = Buffer.allocUnsafe(Math.min(NATIVE_READ_CHUNK_BYTES, maxBytes));
|
|
7347
|
+
while (!stopped && !signal.aborted && bytesReadTotal < maxBytes) {
|
|
7348
|
+
const remaining = maxBytes - bytesReadTotal;
|
|
7349
|
+
const { bytesRead } = await file.read(buffer, 0, Math.min(buffer.length, remaining), null);
|
|
7350
|
+
if (bytesRead === 0) break;
|
|
7351
|
+
const chunk = buffer.subarray(0, bytesRead);
|
|
7352
|
+
if (!binaryChecked) {
|
|
7353
|
+
binaryChecked = true;
|
|
7354
|
+
if (isBinaryBuffer(chunk)) return;
|
|
6916
7355
|
}
|
|
7356
|
+
bytesReadTotal += bytesRead;
|
|
7357
|
+
const text = leftover + chunk.toString("utf8");
|
|
7358
|
+
const lines = text.split(/\r?\n/);
|
|
7359
|
+
leftover = lines.pop() ?? "";
|
|
7360
|
+
for (const rawLine of lines) {
|
|
7361
|
+
if (stopped || signal.aborted) break;
|
|
7362
|
+
lineNumber++;
|
|
7363
|
+
const ln = capSubject(rawLine);
|
|
7364
|
+
re.lastIndex = 0;
|
|
7365
|
+
if (!re.test(ln)) continue;
|
|
7366
|
+
fileHits++;
|
|
7367
|
+
total++;
|
|
7368
|
+
if (mode === "content") {
|
|
7369
|
+
if (matches.length < limit) matches.push(`${full}:${lineNumber}:${ln}`);
|
|
7370
|
+
} else if (mode === "files_with_matches") {
|
|
7371
|
+
if (fileHits === 1 && matches.length < limit) matches.push(full);
|
|
7372
|
+
break;
|
|
7373
|
+
} else if (fileHits === 1 && matches.length < limit) {
|
|
7374
|
+
matches.push(`${full}:${countOnlyFirstHit ? 1 : 0}`);
|
|
7375
|
+
}
|
|
7376
|
+
if (countOnlyFirstHit || mode !== "content" && matches.length >= limit) {
|
|
7377
|
+
stopped = true;
|
|
7378
|
+
break;
|
|
7379
|
+
}
|
|
7380
|
+
}
|
|
7381
|
+
if (mode === "files_with_matches" && fileHits > 0) break;
|
|
6917
7382
|
}
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
7383
|
+
if (!stopped && !signal.aborted && leftover.length > 0) {
|
|
7384
|
+
lineNumber++;
|
|
7385
|
+
const ln = capSubject(leftover);
|
|
7386
|
+
re.lastIndex = 0;
|
|
7387
|
+
if (re.test(ln)) {
|
|
7388
|
+
fileHits++;
|
|
7389
|
+
total++;
|
|
7390
|
+
if (mode === "content") {
|
|
7391
|
+
if (matches.length < limit) matches.push(`${full}:${lineNumber}:${ln}`);
|
|
7392
|
+
} else if (mode === "files_with_matches") {
|
|
7393
|
+
if (matches.length < limit) matches.push(full);
|
|
7394
|
+
} else if (matches.length < limit) {
|
|
7395
|
+
matches.push(`${full}:${countOnlyFirstHit ? 1 : fileHits}`);
|
|
7396
|
+
}
|
|
7397
|
+
}
|
|
6923
7398
|
}
|
|
6924
|
-
if (
|
|
6925
|
-
|
|
7399
|
+
if (fileHits > 0) {
|
|
7400
|
+
if (mode === "count") {
|
|
7401
|
+
const idx = matches.findIndex((entry) => entry.startsWith(`${full}:`));
|
|
7402
|
+
if (idx !== -1) matches[idx] = `${full}:${fileHits}`;
|
|
7403
|
+
}
|
|
7404
|
+
if (mode === "files_with_matches" && matches.length >= limit) stopped = true;
|
|
6926
7405
|
}
|
|
7406
|
+
if (mode === "content" && matches.length >= limit) stopped = true;
|
|
7407
|
+
if (mode === "count" && matches.length >= limit && (countOnlyFirstHit || mode !== "count")) stopped = true;
|
|
7408
|
+
} finally {
|
|
7409
|
+
await file.close();
|
|
6927
7410
|
}
|
|
6928
|
-
if (matches.length >= limit) stopped = true;
|
|
6929
7411
|
} catch {
|
|
6930
7412
|
}
|
|
6931
7413
|
};
|
|
@@ -6938,18 +7420,20 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
6938
7420
|
return;
|
|
6939
7421
|
}
|
|
6940
7422
|
const files = [];
|
|
7423
|
+
const subdirs = [];
|
|
6941
7424
|
for (const e of entries) {
|
|
6942
7425
|
if (stopped) return;
|
|
6943
7426
|
if (DEFAULT_IGNORE3.includes(e.name)) continue;
|
|
6944
7427
|
if (e.isSymbolicLink()) continue;
|
|
6945
7428
|
const full = path3.join(dir, e.name);
|
|
6946
7429
|
if (e.isDirectory()) {
|
|
6947
|
-
|
|
7430
|
+
subdirs.push(full);
|
|
6948
7431
|
} else if (e.isFile()) {
|
|
6949
7432
|
files.push({ full, name: e.name });
|
|
6950
7433
|
}
|
|
6951
7434
|
}
|
|
6952
7435
|
await mapWithConcurrency(files, NATIVE_SCAN_CONCURRENCY, ({ full, name }) => scanFile(full, name));
|
|
7436
|
+
await mapWithConcurrency(subdirs, Math.min(16, NATIVE_SCAN_CONCURRENCY), walk);
|
|
6953
7437
|
};
|
|
6954
7438
|
await walk(base);
|
|
6955
7439
|
return {
|
|
@@ -6959,20 +7443,6 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
6959
7443
|
used: "native"
|
|
6960
7444
|
};
|
|
6961
7445
|
}
|
|
6962
|
-
async function mapWithConcurrency(items, concurrency, fn) {
|
|
6963
|
-
if (items.length === 0) return;
|
|
6964
|
-
let next = 0;
|
|
6965
|
-
const workerCount = Math.min(Math.max(1, concurrency), items.length);
|
|
6966
|
-
const workers = Array.from({ length: workerCount }, async () => {
|
|
6967
|
-
for (; ; ) {
|
|
6968
|
-
const idx = next++;
|
|
6969
|
-
if (idx >= items.length) return;
|
|
6970
|
-
const item = items[idx];
|
|
6971
|
-
if (item !== void 0) await fn(item);
|
|
6972
|
-
}
|
|
6973
|
-
});
|
|
6974
|
-
await Promise.all(workers);
|
|
6975
|
-
}
|
|
6976
7446
|
var installTool = {
|
|
6977
7447
|
name: "install",
|
|
6978
7448
|
category: "Package Management",
|
|
@@ -7097,6 +7567,14 @@ var installTool = {
|
|
|
7097
7567
|
}
|
|
7098
7568
|
}
|
|
7099
7569
|
}
|
|
7570
|
+
ctx.recordSideEffect?.({
|
|
7571
|
+
toolUseId: `install-${Date.now()}`,
|
|
7572
|
+
toolName: "install",
|
|
7573
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7574
|
+
input: { packages: pkgList, cwd, dry_run: Boolean(input.dry_run) },
|
|
7575
|
+
outcome: output.dry_run ? "dry run" : result.exitCode === 0 ? `installed ${pkgList.length || "all"} packages` : `failed (exit ${result.exitCode})`,
|
|
7576
|
+
risk: "package"
|
|
7577
|
+
});
|
|
7100
7578
|
yield { type: "final", output };
|
|
7101
7579
|
}
|
|
7102
7580
|
};
|
|
@@ -7186,8 +7664,8 @@ var jsonTool = {
|
|
|
7186
7664
|
};
|
|
7187
7665
|
}
|
|
7188
7666
|
};
|
|
7189
|
-
function query(data,
|
|
7190
|
-
const parts =
|
|
7667
|
+
function query(data, path22) {
|
|
7668
|
+
const parts = path22.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
7191
7669
|
let current = data;
|
|
7192
7670
|
for (const part of parts) {
|
|
7193
7671
|
if (current === null || current === void 0) return void 0;
|
|
@@ -7464,7 +7942,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
7464
7942
|
}
|
|
7465
7943
|
var DOCKER_LOGS_TIMEOUT_MS = 3e3;
|
|
7466
7944
|
var MAX_TAIL_LINES = 1e5;
|
|
7467
|
-
async function fileLogs(
|
|
7945
|
+
async function fileLogs(path22, lines, filterRe, stream) {
|
|
7468
7946
|
const { createInterface } = await import('node:readline');
|
|
7469
7947
|
const { createReadStream } = await import('node:fs');
|
|
7470
7948
|
const entries = [];
|
|
@@ -7473,7 +7951,7 @@ async function fileLogs(path21, lines, filterRe, stream) {
|
|
|
7473
7951
|
let writeIdx = 0;
|
|
7474
7952
|
let totalLines = 0;
|
|
7475
7953
|
const rl = createInterface({
|
|
7476
|
-
input: createReadStream(
|
|
7954
|
+
input: createReadStream(path22),
|
|
7477
7955
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
7478
7956
|
});
|
|
7479
7957
|
for await (const line of rl) {
|
|
@@ -7494,7 +7972,7 @@ async function fileLogs(path21, lines, filterRe, stream) {
|
|
|
7494
7972
|
if (parsed) entries.push(parsed);
|
|
7495
7973
|
}
|
|
7496
7974
|
return {
|
|
7497
|
-
source:
|
|
7975
|
+
source: path22,
|
|
7498
7976
|
entries,
|
|
7499
7977
|
total: entries.length,
|
|
7500
7978
|
truncated: totalLines > effLines,
|
|
@@ -8017,13 +8495,13 @@ ${formatTaskList(taskFile.tasks)}`
|
|
|
8017
8495
|
}
|
|
8018
8496
|
};
|
|
8019
8497
|
function mkResult(plan, ok, message, todos) {
|
|
8020
|
-
const
|
|
8498
|
+
const open2 = plan.items.filter((i) => i.status !== "done").length;
|
|
8021
8499
|
const result = {
|
|
8022
8500
|
ok,
|
|
8023
8501
|
message,
|
|
8024
8502
|
plan: formatPlan(plan),
|
|
8025
8503
|
count: plan.items.length,
|
|
8026
|
-
open
|
|
8504
|
+
open: open2
|
|
8027
8505
|
};
|
|
8028
8506
|
if (todos !== void 0) result.todos = todos;
|
|
8029
8507
|
return result;
|
|
@@ -10059,7 +10537,7 @@ var writeTool = {
|
|
|
10059
10537
|
if (existed) {
|
|
10060
10538
|
if (!ctx.hasRead(absPath)) {
|
|
10061
10539
|
prev = await fs2.readFile(absPath, "utf8");
|
|
10062
|
-
ctx.recordRead(absPath, stat12.mtimeMs);
|
|
10540
|
+
ctx.recordRead(absPath, stat12.mtimeMs, "write");
|
|
10063
10541
|
} else {
|
|
10064
10542
|
prev = await fs2.readFile(absPath, "utf8");
|
|
10065
10543
|
}
|
|
@@ -10073,7 +10551,7 @@ var writeTool = {
|
|
|
10073
10551
|
const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
10074
10552
|
+ (new file, ${input.content.split("\n").length} lines)`;
|
|
10075
10553
|
const stat11 = await fs2.stat(absPath);
|
|
10076
|
-
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
10554
|
+
ctx.recordRead(absPath, stat11.mtimeMs, "write");
|
|
10077
10555
|
ctx.session.recordFileChange({
|
|
10078
10556
|
path: absPath,
|
|
10079
10557
|
action: existed ? "modified" : "created",
|
|
@@ -10132,7 +10610,8 @@ var TIER2_TOOLS = [
|
|
|
10132
10610
|
planTool,
|
|
10133
10611
|
taskTool,
|
|
10134
10612
|
installTool,
|
|
10135
|
-
auditTool
|
|
10613
|
+
auditTool,
|
|
10614
|
+
designTool
|
|
10136
10615
|
];
|
|
10137
10616
|
var TIER3_TOOLS = [
|
|
10138
10617
|
outdatedTool,
|
|
@@ -10177,6 +10656,7 @@ var builtinTools = [
|
|
|
10177
10656
|
logsTool,
|
|
10178
10657
|
documentTool,
|
|
10179
10658
|
scaffoldTool,
|
|
10659
|
+
designTool,
|
|
10180
10660
|
toolSearchTool,
|
|
10181
10661
|
toolUseTool,
|
|
10182
10662
|
batchToolUseTool,
|