folderview 0.1.0 → 0.2.1
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 +15 -3
- package/dist/client/assets/app.css +1 -1
- package/dist/client/assets/main.js +125 -108
- package/dist/git.js +135 -2
- package/dist/review.js +34 -5
- package/dist/server.js +66 -4
- package/package.json +1 -1
package/dist/git.js
CHANGED
|
@@ -159,8 +159,8 @@ export class Git {
|
|
|
159
159
|
execFile('git', ['show', `${rev}:${this.repoPath(rel)}`], { cwd: this.top, encoding: 'buffer', maxBuffer: 256 * 1024 * 1024, windowsHide: true }, (err, stdout) => err ? reject(err) : resolve(stdout));
|
|
160
160
|
});
|
|
161
161
|
}
|
|
162
|
-
async log(rel, limit = 30) {
|
|
163
|
-
const out = await this.git(['log', '--follow', `-n${limit}`, '--format=%h%x1f%an%x1f%at%x1f%s%x1e', '--', this.repoPath(rel)]).catch(() => '');
|
|
162
|
+
async log(rel, limit = 30, skip = 0) {
|
|
163
|
+
const out = await this.git(['log', '--follow', `-n${limit}`, `--skip=${skip}`, '--format=%h%x1f%an%x1f%at%x1f%s%x1e', '--', this.repoPath(rel)]).catch(() => '');
|
|
164
164
|
return out
|
|
165
165
|
.split('\x1e')
|
|
166
166
|
.map((r) => r.trim())
|
|
@@ -174,6 +174,125 @@ export class Git {
|
|
|
174
174
|
commitDiff(rel, hash) {
|
|
175
175
|
return this.git(['show', '--no-color', '--format=%H%n%an <%ae>%n%ad%n%n%B', '-M', hash, '--', this.repoPath(rel)]);
|
|
176
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* The repository's commits (those touching the served folder), newest first, each marked pushed or local, plus
|
|
179
|
+
* the upstream's commits not pulled yet (on the first page).
|
|
180
|
+
*/
|
|
181
|
+
async history(skip = 0, limit = 50, q = '') {
|
|
182
|
+
const scope = this.prefix ? ['--', this.prefix] : [];
|
|
183
|
+
const none = () => '';
|
|
184
|
+
// a search: words in the message (any case), or the start of a hash
|
|
185
|
+
const find = q ? ['-i', '-F', `--grep=${q}`] : [];
|
|
186
|
+
const first = !skip && !q;
|
|
187
|
+
// every question at once (each git call costs a process start, slow on Windows); the ones that can't apply
|
|
188
|
+
// (no upstream, no remote, no commits yet) just fail to ''
|
|
189
|
+
const [remoteList, up, out, hashOut, inRaw, inCount, localRaw, localScoped] = await Promise.all([
|
|
190
|
+
this.git(['remote']).catch(none),
|
|
191
|
+
this.git(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}']).catch(none),
|
|
192
|
+
this.git(['log', `-n${limit + 1}`, `--skip=${skip}`, `--format=${LOG_FORMAT}`, ...find, 'HEAD', ...scope]).catch(none),
|
|
193
|
+
/^[0-9a-f]{4,40}$/i.test(q) && !skip ? this.git(['log', '-n1', `--format=${LOG_FORMAT}`, q, '--']).catch(none) : '',
|
|
194
|
+
first ? this.git(['log', '-n50', `--format=${LOG_FORMAT}`, 'HEAD..@{u}', ...scope]).catch(none) : '',
|
|
195
|
+
first ? this.git(['rev-list', '--count', 'HEAD..@{u}', ...scope]).catch(none) : '',
|
|
196
|
+
// the commits only here (up to a limit, so a never-pushed giant history stays quick)
|
|
197
|
+
this.git(['rev-list', `--max-count=${LOCAL_CAP + 1}`, 'HEAD', '--not', '--remotes']).catch(none),
|
|
198
|
+
first && this.prefix ? this.git(['rev-list', '--count', `--max-count=${LOCAL_CAP + 1}`, 'HEAD', '--not', '--remotes', ...scope]).catch(none) : '',
|
|
199
|
+
]);
|
|
200
|
+
const remotes = remoteList.trim() !== '';
|
|
201
|
+
const upstream = up.trim() || undefined;
|
|
202
|
+
const h = { commits: [], incoming: [], incomingTotal: 0, more: false, remotes, ...(upstream ? { upstream } : {}) };
|
|
203
|
+
let all = parseLog(out);
|
|
204
|
+
h.more = all.length > limit;
|
|
205
|
+
all = all.slice(0, limit);
|
|
206
|
+
const hit = parseLog(hashOut)[0];
|
|
207
|
+
if (hit && !all.some((c) => c.full === hit.full))
|
|
208
|
+
all.unshift(hit);
|
|
209
|
+
h.commits = all.map((c) => ({ ...c, state: remotes ? 'pushed' : 'unknown' }));
|
|
210
|
+
const localList = localRaw.split('\n').filter(Boolean);
|
|
211
|
+
if (remotes && all.length) {
|
|
212
|
+
let local = new Set(localList);
|
|
213
|
+
if (localList.length > LOCAL_CAP) {
|
|
214
|
+
// more unpushed commits than the list holds: ask again for just this page's stretch of time (the unpushed
|
|
215
|
+
// ones no older than its oldest commit, a day's slack for clock skew)
|
|
216
|
+
const oldest = Math.floor(Math.min(...all.map((c) => c.time)) / 1000 - 86400);
|
|
217
|
+
local = new Set((await this.git(['rev-list', `--since=${oldest}`, 'HEAD', '--not', '--remotes']).catch(none)).split('\n').filter(Boolean));
|
|
218
|
+
}
|
|
219
|
+
for (const c of h.commits)
|
|
220
|
+
if (local.has(c.full))
|
|
221
|
+
c.state = 'local';
|
|
222
|
+
}
|
|
223
|
+
if (upstream) {
|
|
224
|
+
h.incoming = parseLog(inRaw).map((c) => ({ ...c, state: 'incoming' }));
|
|
225
|
+
h.incomingTotal = Number(inCount.trim()) || h.incoming.length;
|
|
226
|
+
}
|
|
227
|
+
if (remotes && first) {
|
|
228
|
+
const n = this.prefix ? Number(localScoped.trim()) || 0 : localList.length;
|
|
229
|
+
h.localTotal = n > LOCAL_CAP ? `${LOCAL_CAP}+` : String(n);
|
|
230
|
+
}
|
|
231
|
+
return h;
|
|
232
|
+
}
|
|
233
|
+
/** what a commit was, and which files (in the served folder) it changed, against its first parent */
|
|
234
|
+
async commitInfo(rev) {
|
|
235
|
+
const raw = await this.git(['show', '-s', '--format=%H%x1f%h%x1f%an%x1f%ae%x1f%at%x1f%P%x1f%s%x1f%b', rev]);
|
|
236
|
+
const [full = '', hash = '', author = '', email = '', time = '0', parents = '', subject = '', ...rest] = raw.replace(/\n$/, '').split('\x1f');
|
|
237
|
+
const base = await this.parentOf(full);
|
|
238
|
+
const scope = this.prefix ? ['--', this.prefix] : [];
|
|
239
|
+
const [names, nums] = await Promise.all([
|
|
240
|
+
this.git(['diff', '--name-status', '-z', '-M', base, full, ...scope]),
|
|
241
|
+
this.git(['diff', '--numstat', '-z', '-M', base, full, ...scope]),
|
|
242
|
+
]);
|
|
243
|
+
const files = [];
|
|
244
|
+
const recs = names.split('\0');
|
|
245
|
+
for (let i = 0; i < recs.length; i++) {
|
|
246
|
+
const s = recs[i];
|
|
247
|
+
if (!s)
|
|
248
|
+
continue;
|
|
249
|
+
let from;
|
|
250
|
+
let file = recs[++i] ?? '';
|
|
251
|
+
if (s[0] === 'R' || s[0] === 'C') {
|
|
252
|
+
from = file;
|
|
253
|
+
file = recs[++i] ?? '';
|
|
254
|
+
}
|
|
255
|
+
const rel = this.fromRepo(file);
|
|
256
|
+
if (rel === undefined)
|
|
257
|
+
continue;
|
|
258
|
+
const state = s[0] === 'A' || s[0] === 'C' ? 'added' : s[0] === 'D' ? 'deleted' : s[0] === 'R' ? 'renamed' : 'modified';
|
|
259
|
+
const f = { path: rel, state };
|
|
260
|
+
if (from)
|
|
261
|
+
f.from = this.fromRepo(from) ?? from;
|
|
262
|
+
files.push(f);
|
|
263
|
+
}
|
|
264
|
+
const byPath = new Map(files.map((f) => [f.path, f]));
|
|
265
|
+
const nr = nums.split('\0');
|
|
266
|
+
for (let i = 0; i < nr.length; i++) {
|
|
267
|
+
const m = /^(\d+|-)\t(\d+|-)\t(.*)$/.exec(nr[i] ?? '');
|
|
268
|
+
if (!m)
|
|
269
|
+
continue;
|
|
270
|
+
let file = m[3] ?? '';
|
|
271
|
+
if (!file) {
|
|
272
|
+
i++;
|
|
273
|
+
file = nr[++i] ?? '';
|
|
274
|
+
}
|
|
275
|
+
const f = byPath.get(this.fromRepo(file) ?? '');
|
|
276
|
+
if (f && m[1] !== '-') {
|
|
277
|
+
f.add = Number(m[1]);
|
|
278
|
+
f.del = Number(m[2]);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
commit: { full, hash, author, email, time: Number(time) * 1000, parents: parents.split(' ').filter(Boolean), subject, body: rest.join('\x1f').trim() },
|
|
283
|
+
files,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/** one file's diff in a commit, against the commit's first parent (a rename needs its old path too) */
|
|
287
|
+
async commitFileDiff(rev, rel, fromRel) {
|
|
288
|
+
const full = (await this.git(['rev-parse', '--verify', rev + '^{commit}'])).trim();
|
|
289
|
+
const base = await this.parentOf(full);
|
|
290
|
+
const paths = [this.repoPath(rel), ...(fromRel ? [this.repoPath(fromRel)] : [])];
|
|
291
|
+
return this.git(['diff', '--no-color', '-M', base, full, '--', ...paths]);
|
|
292
|
+
}
|
|
293
|
+
async parentOf(full) {
|
|
294
|
+
return (await this.git(['rev-parse', '--verify', '-q', full + '^1']).catch(() => '')).trim() || EMPTY_TREE;
|
|
295
|
+
}
|
|
177
296
|
async userName() {
|
|
178
297
|
return (await this.git(['config', 'user.name']).catch(() => '')).trim();
|
|
179
298
|
}
|
|
@@ -218,6 +337,20 @@ function stateOf(x, y) {
|
|
|
218
337
|
return 'added';
|
|
219
338
|
return 'modified';
|
|
220
339
|
}
|
|
340
|
+
const LOCAL_CAP = 1000;
|
|
341
|
+
const LOG_FORMAT = '%H%x1f%h%x1f%an%x1f%at%x1f%P%x1f%s%x1e';
|
|
342
|
+
/** git's empty tree: what a first commit is compared with */
|
|
343
|
+
const EMPTY_TREE = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
|
|
344
|
+
function parseLog(out) {
|
|
345
|
+
return out
|
|
346
|
+
.split('\x1e')
|
|
347
|
+
.map((r) => r.trim())
|
|
348
|
+
.filter(Boolean)
|
|
349
|
+
.map((r) => {
|
|
350
|
+
const [full = '', hash = '', author = '', time = '0', parents = '', subject = ''] = r.split('\x1f');
|
|
351
|
+
return { full, hash, author, time: Number(time) * 1000, parents: parents.split(' ').filter(Boolean), subject };
|
|
352
|
+
});
|
|
353
|
+
}
|
|
221
354
|
const nullDevice = () => (process.platform === 'win32' ? 'NUL' : '/dev/null');
|
|
222
355
|
function run(cwd, args, ok = [0]) {
|
|
223
356
|
return new Promise((resolve, reject) => {
|
package/dist/review.js
CHANGED
|
@@ -107,6 +107,15 @@ export class Review {
|
|
|
107
107
|
if (quote !== undefined)
|
|
108
108
|
c.quote = quote;
|
|
109
109
|
}
|
|
110
|
+
if (input.area)
|
|
111
|
+
c.area = input.area;
|
|
112
|
+
if (input.strokes?.length)
|
|
113
|
+
c.strokes = input.strokes;
|
|
114
|
+
if (input.snapshot) {
|
|
115
|
+
fs.mkdirSync(path.join(this.dir, 'images'), { recursive: true });
|
|
116
|
+
c.image = `images/${c.id}.png`;
|
|
117
|
+
fs.writeFileSync(path.join(this.dir, c.image), input.snapshot);
|
|
118
|
+
}
|
|
110
119
|
if (input.commit)
|
|
111
120
|
c.commit = input.commit;
|
|
112
121
|
this.comments.push(c);
|
|
@@ -135,19 +144,35 @@ export class Review {
|
|
|
135
144
|
return c;
|
|
136
145
|
}
|
|
137
146
|
remove(id) {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
if (this.comments.length === n)
|
|
147
|
+
const gone = this.comments.find((c) => c.id === id);
|
|
148
|
+
if (!gone)
|
|
141
149
|
return false;
|
|
150
|
+
this.comments = this.comments.filter((c) => c.id !== id);
|
|
151
|
+
this.dropImage(gone);
|
|
142
152
|
this.save();
|
|
143
153
|
return true;
|
|
144
154
|
}
|
|
145
155
|
clearResolved() {
|
|
146
156
|
const n = this.comments.length;
|
|
157
|
+
for (const c of this.comments)
|
|
158
|
+
if (c.status === 'resolved')
|
|
159
|
+
this.dropImage(c);
|
|
147
160
|
this.comments = this.comments.filter((c) => c.status !== 'resolved');
|
|
148
161
|
this.save();
|
|
149
162
|
return n - this.comments.length;
|
|
150
163
|
}
|
|
164
|
+
/** the marked-up picture of an image comment, as a path inside the review folder (or undefined) */
|
|
165
|
+
imagePath(id) {
|
|
166
|
+
const c = this.comments.find((x) => x.id === id);
|
|
167
|
+
if (!c?.image || !/^images\/[0-9a-f]+\.png$/.test(c.image))
|
|
168
|
+
return undefined;
|
|
169
|
+
const abs = path.join(this.dir, c.image);
|
|
170
|
+
return fs.existsSync(abs) ? abs : undefined;
|
|
171
|
+
}
|
|
172
|
+
dropImage(c) {
|
|
173
|
+
if (c.image && /^images\/[0-9a-f]+\.png$/.test(c.image))
|
|
174
|
+
fs.rmSync(path.join(this.dir, c.image), { force: true });
|
|
175
|
+
}
|
|
151
176
|
quote(rel, from, to) {
|
|
152
177
|
try {
|
|
153
178
|
const abs = path.join(this.root, rel);
|
|
@@ -202,7 +227,7 @@ export function toMarkdown(comments, jsonRel, { openOnly = false } = {}) {
|
|
|
202
227
|
'',
|
|
203
228
|
'## How to work through this (for an agent)',
|
|
204
229
|
'',
|
|
205
|
-
'- Each comment is on a file,
|
|
230
|
+
'- Each comment is on a file, on lines of it (`path:line`), or on an area of an image (marked by hand; `images/<id>.png` beside this file shows the marks). Paths are relative to the folder folderview serves.',
|
|
206
231
|
'- The quoted code is what the lines said when the comment was left; the file may have moved on since, so find the code, not just the line number.',
|
|
207
232
|
`- When a comment is dealt with, set its \`"status"\` to \`"resolved"\` in \`${jsonRel}\` and, if there is something to say, add a reply:`,
|
|
208
233
|
' `"replies": [{ "id": "a1", "author": "agent", "body": "Done: …", "at": "<ISO time>" }]`. The reviewer sees it live.',
|
|
@@ -219,10 +244,14 @@ export function toMarkdown(comments, jsonRel, { openOnly = false } = {}) {
|
|
|
219
244
|
file = c.path;
|
|
220
245
|
out.push(`### ${file || '(the whole folder)'}`, '');
|
|
221
246
|
}
|
|
222
|
-
const where = c.line ? `${c.path}:${c.line}${c.endLine ? '-' + c.endLine : ''}` : `${c.path || '.'} (whole file)`;
|
|
247
|
+
const where = c.line ? `${c.path}:${c.line}${c.endLine ? '-' + c.endLine : ''}` : c.area ? `${c.path} (marked area)` : `${c.path || '.'} (whole file)`;
|
|
223
248
|
out.push(`#### \`${where}\` — ${c.author}${c.commit ? ` @ ${c.commit}` : ''} · id \`${c.id}\``, '');
|
|
224
249
|
if (c.quote)
|
|
225
250
|
out.push('```' + langOf(c.path), c.quote, '```', '');
|
|
251
|
+
if (c.area)
|
|
252
|
+
out.push(`Marked area: x ${c.area.x}, y ${c.area.y}, ${c.area.w} × ${c.area.h} px of the image.` + (c.image ? ` The marks are drawn on a copy: ${c.image} (beside this file).` : ''), '');
|
|
253
|
+
if (c.image)
|
|
254
|
+
out.push(``, '');
|
|
226
255
|
out.push(c.body, '');
|
|
227
256
|
for (const r of c.replies ?? [])
|
|
228
257
|
out.push(`> **${r.author}**: ${r.body.replace(/\n/g, '\n> ')}`, '');
|
package/dist/server.js
CHANGED
|
@@ -219,7 +219,30 @@ export async function start(opts) {
|
|
|
219
219
|
return void res.end(buf);
|
|
220
220
|
}
|
|
221
221
|
case '/api/git/log':
|
|
222
|
-
|
|
222
|
+
{
|
|
223
|
+
// a page of the file's commits, and whether there are more
|
|
224
|
+
const all = await git.log(rel, 31, Math.max(0, Number(q('skip')) || 0));
|
|
225
|
+
return json(res, 200, { commits: all.slice(0, 30), more: all.length > 30 });
|
|
226
|
+
}
|
|
227
|
+
case '/api/git/history':
|
|
228
|
+
return json(res, 200, await git.history(Math.max(0, Number(q('skip')) || 0), 50, q('q').trim().slice(0, 200)));
|
|
229
|
+
case '/api/git/commit-info': {
|
|
230
|
+
const h = q('h');
|
|
231
|
+
if (!isRev(h))
|
|
232
|
+
return json(res, 400, { error: 'bad revision' });
|
|
233
|
+
const info = await git.commitInfo(h);
|
|
234
|
+
info.files = info.files.filter((f) => visible(f.path) && (!reviewPrefix || !f.path.startsWith(reviewPrefix)));
|
|
235
|
+
return json(res, 200, info);
|
|
236
|
+
}
|
|
237
|
+
case '/api/git/commit-diff': {
|
|
238
|
+
const h = q('h');
|
|
239
|
+
const from = q('from');
|
|
240
|
+
if (!isRev(h))
|
|
241
|
+
return json(res, 400, { error: 'bad revision' });
|
|
242
|
+
if (from && inside(scope, from) === undefined)
|
|
243
|
+
return json(res, 404, { error: 'not found' });
|
|
244
|
+
return text(res, await git.commitFileDiff(h, rel, from || undefined));
|
|
245
|
+
}
|
|
223
246
|
case '/api/git/commit': {
|
|
224
247
|
const h = q('h');
|
|
225
248
|
if (!isRev(h))
|
|
@@ -234,8 +257,16 @@ export async function start(opts) {
|
|
|
234
257
|
}
|
|
235
258
|
if (p === '/api/comments' && method === 'GET')
|
|
236
259
|
return json(res, 200, { comments: review.all() });
|
|
260
|
+
const img = /^\/api\/comments\/([0-9a-f]+)\/image\.png$/.exec(p);
|
|
261
|
+
if (img) {
|
|
262
|
+
const abs = review.imagePath(img[1] ?? '');
|
|
263
|
+
if (!abs)
|
|
264
|
+
return json(res, 404, { error: 'not found' });
|
|
265
|
+
res.writeHead(200, { 'content-type': 'image/png', 'cache-control': 'no-store', 'x-content-type-options': 'nosniff' });
|
|
266
|
+
return void fs.createReadStream(abs).pipe(res);
|
|
267
|
+
}
|
|
237
268
|
if (p === '/api/comments' && method === 'POST') {
|
|
238
|
-
const b = await body(req);
|
|
269
|
+
const b = await body(req, 16_000_000);
|
|
239
270
|
const rel = str(b['path']);
|
|
240
271
|
if (inside(scope, rel) === undefined)
|
|
241
272
|
return json(res, 400, { error: 'No such file' });
|
|
@@ -248,6 +279,7 @@ export async function start(opts) {
|
|
|
248
279
|
author: str(b['author']) || author,
|
|
249
280
|
...(line ? { line } : {}),
|
|
250
281
|
...(endLine ? { endLine } : {}),
|
|
282
|
+
...imageMarks(b),
|
|
251
283
|
...(git ? { commit: (await gitStatus().catch(() => undefined))?.head ?? '' } : {}),
|
|
252
284
|
});
|
|
253
285
|
return json(res, 201, c);
|
|
@@ -390,7 +422,37 @@ function json(res, code, data) {
|
|
|
390
422
|
function text(res, body, type = 'text/plain; charset=utf-8') {
|
|
391
423
|
res.writeHead(200, { 'content-type': type, 'cache-control': 'no-store' }).end(body);
|
|
392
424
|
}
|
|
393
|
-
|
|
425
|
+
/** an image comment's marks: the area, the strokes, and the marked-up picture (a PNG data URL) */
|
|
426
|
+
function imageMarks(b) {
|
|
427
|
+
const out = {};
|
|
428
|
+
const a = b['area'];
|
|
429
|
+
if (a && typeof a === 'object' && !Array.isArray(a)) {
|
|
430
|
+
const r = Object.fromEntries(Object.entries(a));
|
|
431
|
+
const n = (k) => (typeof r[k] === 'number' && Number.isFinite(r[k]) && Number(r[k]) >= 0 ? Math.round(Number(r[k])) : undefined);
|
|
432
|
+
const [x, y, w, h] = [n('x'), n('y'), n('w'), n('h')];
|
|
433
|
+
if (x !== undefined && y !== undefined && w && h)
|
|
434
|
+
out.area = { x, y, w, h };
|
|
435
|
+
}
|
|
436
|
+
const s = b['strokes'];
|
|
437
|
+
if (Array.isArray(s)) {
|
|
438
|
+
let budget = 20_000;
|
|
439
|
+
const strokes = s
|
|
440
|
+
.filter((st) => Array.isArray(st))
|
|
441
|
+
.map((st) => st.filter((v) => typeof v === 'number' && v >= 0 && v <= 1).map((v) => Math.round(v * 10000) / 10000))
|
|
442
|
+
.filter((st) => st.length >= 2 && st.length % 2 === 0 && (budget -= st.length) >= 0);
|
|
443
|
+
if (strokes.length)
|
|
444
|
+
out.strokes = strokes;
|
|
445
|
+
}
|
|
446
|
+
const snap = typeof b['snapshot'] === 'string' ? /^data:image\/png;base64,([A-Za-z0-9+/=]+)$/.exec(b['snapshot']) : null;
|
|
447
|
+
if (snap && out.strokes) {
|
|
448
|
+
const buf = Buffer.from(snap[1] ?? '', 'base64');
|
|
449
|
+
// a PNG, and nothing else
|
|
450
|
+
if (buf.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])))
|
|
451
|
+
out.snapshot = buf;
|
|
452
|
+
}
|
|
453
|
+
return out;
|
|
454
|
+
}
|
|
455
|
+
async function body(req, max = 1_000_000) {
|
|
394
456
|
if (!String(req.headers['content-type'] ?? '').includes('application/json'))
|
|
395
457
|
throw new Error('Send JSON');
|
|
396
458
|
const chunks = [];
|
|
@@ -398,7 +460,7 @@ async function body(req) {
|
|
|
398
460
|
for await (const c of req) {
|
|
399
461
|
const b = Buffer.isBuffer(c) ? c : Buffer.from(String(c));
|
|
400
462
|
size += b.length;
|
|
401
|
-
if (size >
|
|
463
|
+
if (size > max)
|
|
402
464
|
throw new Error('Too large');
|
|
403
465
|
chunks.push(b);
|
|
404
466
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "folderview",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Browse, search and review any folder in your browser: highlighted code, rendered markdown with mermaid and math, git status and diffs, full-text search, downloads, and review comments saved where an LLM can read them.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|