flowviant 0.78.0 → 0.79.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 +5 -1
- package/bin/lib/authproxy.mjs +101 -32
- package/bin/lib/claude.mjs +1 -18
- package/bin/lib/deploy.mjs +13 -1
- package/bin/lib/env.mjs +37 -24
- package/bin/lib/fleet.mjs +26 -37
- package/bin/lib/git.mjs +0 -263
- package/bin/lib/grant.mjs +24 -3
- package/bin/lib/landed.mjs +69 -22
- package/bin/lib/preview.mjs +35 -16
- package/bin/lib/prompts.mjs +25 -26
- package/bin/lib/shipSweep.mjs +16 -5
- package/bin/lib/work.mjs +411 -50
- package/bin/lib/worktreeDiff.mjs +55 -20
- package/package.json +1 -1
package/bin/lib/worktreeDiff.mjs
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
import { existsSync, statSync, readFileSync } from 'node:fs';
|
|
29
29
|
import { join } from 'node:path';
|
|
30
|
-
import { git } from './git.mjs';
|
|
30
|
+
import { git, gitRaw, splitNul } from './git.mjs';
|
|
31
31
|
|
|
32
32
|
/** Rows reported. The rail shows a handful; the totals below cover the rest. */
|
|
33
33
|
const MAX_FILES = 20;
|
|
@@ -70,6 +70,14 @@ export function taskIdsFromMessage(body) {
|
|
|
70
70
|
return ids;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* The %x1e/%x1f record delimiters, removed from any field that survives
|
|
75
|
+
* parsing. Splitting alone is not enough: git PRESERVES both bytes in a commit
|
|
76
|
+
* body, so a surviving field can still carry a fragment of a forged record,
|
|
77
|
+
* and a stripped field can never re-assemble one downstream.
|
|
78
|
+
*/
|
|
79
|
+
export const stripDelims = (s) => String(s ?? '').replace(/[\x1e\x1f]/g, '');
|
|
80
|
+
|
|
73
81
|
/**
|
|
74
82
|
* The commits this branch has that base does not, with the cards they name.
|
|
75
83
|
*
|
|
@@ -81,14 +89,27 @@ export function taskIdsFromMessage(body) {
|
|
|
81
89
|
* `--no-merges`, because a merge commit describes a range rather than doing
|
|
82
90
|
* work, and its trailer (if it has one) would double-book the range's own
|
|
83
91
|
* commits.
|
|
92
|
+
*
|
|
93
|
+
* THE SHA LIST COMES FROM REV-LIST, NEVER FROM THE FORMATTED LOG. The %x1e and
|
|
94
|
+
* %x1f delimiters are formatting, not a fence: git preserves both bytes inside
|
|
95
|
+
* a commit BODY (verified empirically), so a crafted message can fabricate
|
|
96
|
+
* whole records — a self-named sha carrying task ids, exactly the receipt this
|
|
97
|
+
* product refuses to let an agent write about itself. rev-list prints nothing
|
|
98
|
+
* an author controls, so its output is the set of commits that exist: a parsed
|
|
99
|
+
* record whose sha is not in that set is a forgery and is dropped, a repeated
|
|
100
|
+
* sha is the same forgery wearing a real commit's name, and the delimiter
|
|
101
|
+
* bytes are stripped from every surviving field.
|
|
84
102
|
*/
|
|
85
103
|
function branchCommits(wt, base) {
|
|
86
104
|
if (!base) return [];
|
|
87
105
|
const out = [];
|
|
88
106
|
try {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
107
|
+
const real = new Set(
|
|
108
|
+
git(['rev-list', '--no-merges', '-n', String(MAX_COMMITS), `${base}..HEAD`], wt)
|
|
109
|
+
.split('\n')
|
|
110
|
+
.filter(Boolean)
|
|
111
|
+
);
|
|
112
|
+
if (real.size === 0) return [];
|
|
92
113
|
const raw = git(
|
|
93
114
|
[
|
|
94
115
|
'log',
|
|
@@ -103,9 +124,10 @@ function branchCommits(wt, base) {
|
|
|
103
124
|
for (const rec of raw.split('\x1e')) {
|
|
104
125
|
const line = rec.replace(/^\n+/, '');
|
|
105
126
|
if (!line.trim()) continue;
|
|
106
|
-
const [sha, subject, author, at,
|
|
107
|
-
if (!sha) continue;
|
|
108
|
-
|
|
127
|
+
const [sha, subject, author, at, ...bodyParts] = line.split('\x1f');
|
|
128
|
+
if (!real.has(sha)) continue;
|
|
129
|
+
real.delete(sha);
|
|
130
|
+
const taskIds = taskIdsFromMessage(stripDelims(bodyParts.join('\n')));
|
|
109
131
|
if (taskIds.length === 0) continue;
|
|
110
132
|
let additions = 0;
|
|
111
133
|
let deletions = 0;
|
|
@@ -125,9 +147,9 @@ function branchCommits(wt, base) {
|
|
|
125
147
|
// Clamped to the server's zod caps, same rule as everything else in
|
|
126
148
|
// this file: one over-cap string 400s the whole batch.
|
|
127
149
|
sha: sha.slice(0, 64),
|
|
128
|
-
subject: (subject
|
|
129
|
-
author: (author
|
|
130
|
-
at: (at
|
|
150
|
+
subject: stripDelims(subject).slice(0, 200),
|
|
151
|
+
author: stripDelims(author).slice(0, 80),
|
|
152
|
+
at: stripDelims(at).slice(0, 40),
|
|
131
153
|
additions,
|
|
132
154
|
deletions,
|
|
133
155
|
taskIds: taskIds.slice(0, 8),
|
|
@@ -202,13 +224,25 @@ export function worktreeDiff(wt, baseRef) {
|
|
|
202
224
|
};
|
|
203
225
|
|
|
204
226
|
// Tracked: working tree vs base. `git diff <base>` (no --cached, no second
|
|
205
|
-
// ref) is exactly "everything this session did", committed or not.
|
|
227
|
+
// ref) is exactly "everything this session did", committed or not. `-z`, for
|
|
228
|
+
// the reason gitRaw exists: git's LINE-based output C-quotes any path that
|
|
229
|
+
// is not plain ASCII, so an accented filename arrives as "n\303\251w.txt" —
|
|
230
|
+
// a string that is not the path and reads as escaped garbage in the rail.
|
|
206
231
|
try {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
232
|
+
// `--numstat -z` frames a normal change as one field, "added\tdeleted\tpath",
|
|
233
|
+
// but a RENAME as three: "added\tdeleted\t" (empty path), then the old path,
|
|
234
|
+
// then the new one. An empty path is therefore the rename marker and the
|
|
235
|
+
// next two fields belong to it — read line-wise, a rename would report a
|
|
236
|
+
// file literally named "old => new".
|
|
237
|
+
const fields = splitNul(gitRaw(['diff', '--numstat', '-z', base || 'HEAD', '--'], wt));
|
|
238
|
+
for (let i = 0; i < fields.length; i++) {
|
|
239
|
+
const [a, d, ...rest] = fields[i].split('\t');
|
|
240
|
+
let path = rest.join('\t');
|
|
241
|
+
if (!path) {
|
|
242
|
+
path = fields[i + 2] ?? fields[i + 1]; // the post-rename name is what exists now
|
|
243
|
+
i += 2;
|
|
244
|
+
if (!path) continue;
|
|
245
|
+
}
|
|
212
246
|
const binary = a === '-' || d === '-';
|
|
213
247
|
push(path, binary ? 0 : Number(a) || 0, binary ? 0 : Number(d) || 0, binary);
|
|
214
248
|
}
|
|
@@ -217,11 +251,12 @@ export function worktreeDiff(wt, baseRef) {
|
|
|
217
251
|
}
|
|
218
252
|
|
|
219
253
|
// Untracked, minus everything gitignored — new files are the most visible
|
|
220
|
-
// work a session does and they would otherwise show as nothing at all.
|
|
254
|
+
// work a session does and they would otherwise show as nothing at all. `-z`
|
|
255
|
+
// here is not cosmetic: a C-quoted untracked name fails the statSync below,
|
|
256
|
+
// and the catch swallowed the row — a session's brand-new `café.txt` simply
|
|
257
|
+
// vanished from the rail.
|
|
221
258
|
try {
|
|
222
|
-
const others =
|
|
223
|
-
.split('\n')
|
|
224
|
-
.filter(Boolean);
|
|
259
|
+
const others = splitNul(gitRaw(['ls-files', '--others', '--exclude-standard', '-z'], wt));
|
|
225
260
|
for (const path of others.slice(0, MAX_UNTRACKED_SCAN)) {
|
|
226
261
|
try {
|
|
227
262
|
const full = join(wt, path);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.79.0",
|
|
4
4
|
"description": "Run your own coding CLIs as build agents for Flowviant — Claude Code, Codex or Antigravity, on your own credentials. Holds your sessions, keeps a worktree per tab, and ships branches on your word.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|