@cruxy/cli 1.3.0 → 1.5.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/agent/status.js +96 -0
- package/dist/cli/commands/run.js +54 -3
- package/dist/cli/session-commands.js +16 -49
- package/dist/components/keys.js +38 -0
- package/dist/config/effective.js +225 -0
- package/dist/config/index.js +1 -0
- package/dist/config/manager.js +50 -20
- package/dist/errors/constructors.js +90 -10
- package/dist/limits/cache.js +100 -0
- package/dist/limits/index.js +11 -0
- package/dist/limits/reduce.js +172 -0
- package/dist/limits/types.js +25 -0
- package/dist/onboarding/detect.js +95 -9
- package/dist/onboarding/types.js +29 -1
- package/dist/render/diff.js +7 -1
- package/dist/render/status-view.js +58 -6
- package/dist/theme/tokens.js +7 -0
- package/dist/tui/app.js +125 -2
- package/dist/tui/disk-status.js +47 -0
- package/dist/tui/git-status.js +46 -1
- package/dist/tui/git-view.js +121 -0
- package/dist/tui/index.js +8 -2
- package/dist/tui/layout.js +46 -0
- package/dist/tui/limits-panel.js +255 -0
- package/dist/tui/overview.js +61 -0
- package/dist/tui/panels.js +2 -0
- package/dist/tui/renderer.js +431 -20
- package/dist/tui/settings-view.js +282 -0
- package/dist/tui/tasks-view.js +215 -0
- package/dist/tui/views.js +66 -0
- package/dist/utils/disk.js +95 -0
- package/dist/utils/git.js +113 -0
- package/package.json +2 -2
package/dist/utils/git.js
CHANGED
|
@@ -41,6 +41,79 @@ function runGitAsync(args, cwd) {
|
|
|
41
41
|
export function countChanges(status) {
|
|
42
42
|
return status.split("\n").filter((line) => line.trim() !== "").length;
|
|
43
43
|
}
|
|
44
|
+
/** Porcelain XY code → the status this reports. */
|
|
45
|
+
function statusFromCode(x, y) {
|
|
46
|
+
if (x === "?" || y === "?")
|
|
47
|
+
return "untracked";
|
|
48
|
+
// A `U` on either side, or the AA/DD pairs, are unmerged paths.
|
|
49
|
+
if (x === "U" ||
|
|
50
|
+
y === "U" ||
|
|
51
|
+
(x === "A" && y === "A") ||
|
|
52
|
+
(x === "D" && y === "D")) {
|
|
53
|
+
return "conflicted";
|
|
54
|
+
}
|
|
55
|
+
if (x === "R")
|
|
56
|
+
return "renamed";
|
|
57
|
+
if (x === "A")
|
|
58
|
+
return "added";
|
|
59
|
+
if (x === "D" || y === "D")
|
|
60
|
+
return "deleted";
|
|
61
|
+
return "modified";
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Parse `git status --porcelain` into one entry per path.
|
|
65
|
+
*
|
|
66
|
+
* The format is `XY <path>`, with a rename written `R old -> new`. Renames
|
|
67
|
+
* take the NEW path because that is where the content lives now and what a
|
|
68
|
+
* follow-up diff or edit would name; the old one is not shown, which is a real
|
|
69
|
+
* (small) loss of information accepted to keep one row per change.
|
|
70
|
+
*
|
|
71
|
+
* Paths containing spaces are handled because the split is positional (columns
|
|
72
|
+
* 0-1 are the code, 3 onward is the path) rather than whitespace-delimited.
|
|
73
|
+
* Quoted paths — git quotes non-ASCII unless `core.quotePath=false` — are left
|
|
74
|
+
* exactly as git wrote them rather than half-unescaped into something that
|
|
75
|
+
* looks like a real path but is not.
|
|
76
|
+
*/
|
|
77
|
+
export function parsePorcelain(text) {
|
|
78
|
+
const out = [];
|
|
79
|
+
for (const line of text.split("\n")) {
|
|
80
|
+
if (line.trim() === "")
|
|
81
|
+
continue;
|
|
82
|
+
const x = line[0] ?? " ";
|
|
83
|
+
const y = line[1] ?? " ";
|
|
84
|
+
let path = line.slice(3);
|
|
85
|
+
const status = statusFromCode(x, y);
|
|
86
|
+
if (status === "renamed") {
|
|
87
|
+
const arrow = path.indexOf(" -> ");
|
|
88
|
+
if (arrow >= 0)
|
|
89
|
+
path = path.slice(arrow + 4);
|
|
90
|
+
}
|
|
91
|
+
out.push({ path, status, added: null, removed: null, approximate: false });
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Parse `git diff --numstat` into per-path line counts.
|
|
97
|
+
*
|
|
98
|
+
* A binary file is reported as `-\t-\t<path>`, which becomes `null` on both
|
|
99
|
+
* sides — the honest answer, and the one {@link GitFileChange} already models.
|
|
100
|
+
*/
|
|
101
|
+
export function parseNumstat(text) {
|
|
102
|
+
const out = new Map();
|
|
103
|
+
for (const line of text.split("\n")) {
|
|
104
|
+
if (line.trim() === "")
|
|
105
|
+
continue;
|
|
106
|
+
const [addedRaw, removedRaw, ...rest] = line.split("\t");
|
|
107
|
+
const path = rest.join("\t");
|
|
108
|
+
if (path === "")
|
|
109
|
+
continue;
|
|
110
|
+
out.set(path, {
|
|
111
|
+
added: addedRaw === "-" ? null : Number(addedRaw),
|
|
112
|
+
removed: removedRaw === "-" ? null : Number(removedRaw),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
44
117
|
/** Shared shaping so the sync and async paths cannot disagree. */
|
|
45
118
|
function toGitInfo(branch, status) {
|
|
46
119
|
const changed = countChanges(status);
|
|
@@ -87,3 +160,43 @@ export async function getGitInfoAsync(cwd) {
|
|
|
87
160
|
return null;
|
|
88
161
|
return toGitInfo(branch, status);
|
|
89
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* {@link getGitInfoAsync} plus the per-path detail the Git view needs (P7
|
|
165
|
+
* track 4): the same branch and count, and a `files` list.
|
|
166
|
+
*
|
|
167
|
+
* A SEPARATE ENTRY POINT rather than an option on the old one, because the
|
|
168
|
+
* extra `--numstat` is a third subprocess and the rail must not start paying
|
|
169
|
+
* for a list it has one line to show. Consumers pick the probe that matches
|
|
170
|
+
* what they render, and `files` being absent on the cheap one is what makes
|
|
171
|
+
* that choice visible in the type.
|
|
172
|
+
*
|
|
173
|
+
* The numstat is diffed against HEAD so staged and unstaged work both count —
|
|
174
|
+
* the question a Git view answers is "what has this session done to my files",
|
|
175
|
+
* and whether a change happens to be staged is not part of it. Untracked files
|
|
176
|
+
* appear in the porcelain but in no diff, so their counts stay `null` rather
|
|
177
|
+
* than being invented from a file read.
|
|
178
|
+
*/
|
|
179
|
+
export async function getGitTreeAsync(cwd) {
|
|
180
|
+
const branch = await runGitAsync(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
181
|
+
if (branch === null)
|
|
182
|
+
return null;
|
|
183
|
+
const status = await runGitAsync(["status", "--porcelain"], cwd);
|
|
184
|
+
if (status === null)
|
|
185
|
+
return null;
|
|
186
|
+
const info = toGitInfo(branch, status);
|
|
187
|
+
const files = parsePorcelain(status);
|
|
188
|
+
// Best-effort: a repo with no commits yet has no HEAD to diff against, and
|
|
189
|
+
// that must degrade to "counts unknown", not to "not a repo".
|
|
190
|
+
const numstat = await runGitAsync(["diff", "--numstat", "HEAD"], cwd);
|
|
191
|
+
if (numstat !== null) {
|
|
192
|
+
const counts = parseNumstat(numstat);
|
|
193
|
+
for (const file of files) {
|
|
194
|
+
const c = counts.get(file.path);
|
|
195
|
+
if (c) {
|
|
196
|
+
file.added = c.added;
|
|
197
|
+
file.removed = c.removed;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return { ...info, files };
|
|
202
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cruxy/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "an agentic coding CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"undici": "^6.21.0",
|
|
37
37
|
"zod": "^3.23.8",
|
|
38
38
|
"zod-to-json-schema": "^3.23.5",
|
|
39
|
-
"@cruxy/sdk": "0.
|
|
39
|
+
"@cruxy/sdk": "0.5.0"
|
|
40
40
|
},
|
|
41
41
|
"optionalDependencies": {
|
|
42
42
|
"better-sqlite3": "^12.11.1"
|