@giovannijecha/jecode 0.3.0 → 0.3.2

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.
@@ -4,7 +4,7 @@
4
4
  // from replacing unrelated settings, API keys, or rotated OAuth credentials
5
5
  // with snapshots they cached before the other process wrote.
6
6
  import { randomUUID } from "node:crypto";
7
- import { mkdir, open, readFile, rename, rmdir, stat, unlink } from "node:fs/promises";
7
+ import { mkdir, open, readFile, rmdir, stat, unlink } from "node:fs/promises";
8
8
  import * as path from "node:path";
9
9
  const WAIT_MS = 50;
10
10
  const WAIT_LIMIT_MS = 20_000;
@@ -59,10 +59,12 @@ async function recoverStale(directory) {
59
59
  const details = await stat(directory);
60
60
  if (Date.now() - details.mtimeMs < STALE_MS)
61
61
  return;
62
- const quarantined = `${directory}.${randomUUID()}.stale`;
63
- await rename(directory, quarantined);
64
- await unlink(path.join(quarantined, "owner")).catch(() => undefined);
65
- await rmdir(quarantined).catch(() => undefined);
62
+ if (await ownerIsAlive(directory))
63
+ return;
64
+ // Remove the owner first, then the now-empty directory. `rmdir` cannot
65
+ // erase a fresh lock acquired after another waiter wins this recovery,
66
+ // whereas renaming the shared path can steal that new lock in an ABA race.
67
+ await removeLock(directory);
66
68
  }
67
69
  catch (error) {
68
70
  const code = error.code;
@@ -70,6 +72,36 @@ async function recoverStale(directory) {
70
72
  throw error;
71
73
  }
72
74
  }
75
+ async function ownerIsAlive(directory) {
76
+ let token;
77
+ try {
78
+ token = await readFile(path.join(directory, "owner"), "utf8");
79
+ }
80
+ catch (error) {
81
+ const code = error.code;
82
+ if (code === "ENOENT")
83
+ return false;
84
+ if (code === "EACCES" || code === "EPERM")
85
+ return true;
86
+ throw error;
87
+ }
88
+ const match = /^([1-9]\d*):/.exec(token.trim());
89
+ if (match === null)
90
+ return false;
91
+ const pid = Number(match[1]);
92
+ if (!Number.isSafeInteger(pid) || pid > 0x7fff_ffff)
93
+ return false;
94
+ try {
95
+ process.kill(pid, 0);
96
+ return true;
97
+ }
98
+ catch (error) {
99
+ // ESRCH is the one portable proof that the owner no longer exists.
100
+ // Permission failures and unknown platform errors must not authorize a
101
+ // second writer to enter the same store.
102
+ return error.code !== "ESRCH";
103
+ }
104
+ }
73
105
  async function release(directory, token) {
74
106
  try {
75
107
  const owner = path.join(directory, "owner");
@@ -102,6 +102,7 @@ function searchBatch(executable, files, options) {
102
102
  let buffered = "";
103
103
  let stderr = "";
104
104
  let invalid = false;
105
+ let overLimit = false;
105
106
  let settled = false;
106
107
  const onAbort = () => child.kill();
107
108
  options.signal?.addEventListener("abort", onAbort, { once: true });
@@ -118,13 +119,23 @@ function searchBatch(executable, files, options) {
118
119
  resolve(value);
119
120
  };
120
121
  const consume = (line) => {
121
- if (line === "" || invalid)
122
+ if (line === "" || invalid || overLimit)
122
123
  return;
123
124
  try {
124
125
  const event = JSON.parse(line);
125
126
  const parsed = ripgrepEvent(event);
126
- if (parsed?.kind === "match")
127
+ if (parsed?.kind === "match") {
128
+ // `rg --max-count` is per file, not global. Once the raw stream
129
+ // exceeds the requested result count, stop the accelerator and let
130
+ // the portable scanner produce the exact bounded answer. Returning
131
+ // early here would lose the later binary-file end marker.
132
+ if (matches.length >= options.limit) {
133
+ overLimit = true;
134
+ child.kill();
135
+ return;
136
+ }
127
137
  matches.push(parsed.match);
138
+ }
128
139
  if (parsed?.kind === "binary")
129
140
  binaryPaths.add(parsed.path);
130
141
  }
@@ -166,7 +177,7 @@ function searchBatch(executable, files, options) {
166
177
  return;
167
178
  }
168
179
  consume(buffered);
169
- if (invalid || (code !== 0 && code !== 1) || stderr.trim() !== "") {
180
+ if (invalid || overLimit || (code !== 0 && code !== 1) || stderr.trim() !== "") {
170
181
  finish(undefined);
171
182
  return;
172
183
  }
@@ -2,7 +2,6 @@
2
2
  import { hasColor, row } from "../../ui/render.js";
3
3
  const OUTPUT_ROWS = 8;
4
4
  const LIVE_OUTPUT_ROWS = 6;
5
- const DIFF_ROWS = 12;
6
5
  export function renderTool(block, width, pal, context = {}) {
7
6
  const shown = visibleDetails(block);
8
7
  const right = liveLabel(block, context);
@@ -31,54 +30,9 @@ function visibleDetails(block) {
31
30
  : `… ${hidden} earlier lines · ctrl+o expand`;
32
31
  return [{ kind: "gap", text: note }, ...all.slice(-limit)];
33
32
  }
34
- return diffWindow(all);
35
- }
36
- /** Keep change rows and their nearest context; never turn a diff into a tail. */
37
- function diffWindow(all) {
38
- if (all.length <= DIFF_ROWS)
39
- return [...all];
40
- const important = all.flatMap((detail, index) => detail.kind === "keep" ? [] : [index]);
41
- const chosen = new Set();
42
- const budgeted = important.length <= DIFF_ROWS
43
- ? important
44
- : [
45
- ...important.slice(0, Math.ceil(DIFF_ROWS / 2)),
46
- ...important.slice(-Math.floor(DIFF_ROWS / 2)),
47
- ];
48
- for (const index of budgeted)
49
- chosen.add(index);
50
- for (let radius = 1; chosen.size < DIFF_ROWS && radius < all.length; radius++) {
51
- for (const index of important) {
52
- for (const candidate of [index - radius, index + radius]) {
53
- if (candidate < 0 || candidate >= all.length || chosen.has(candidate))
54
- continue;
55
- chosen.add(candidate);
56
- if (chosen.size >= DIFF_ROWS)
57
- break;
58
- }
59
- if (chosen.size >= DIFF_ROWS)
60
- break;
61
- }
62
- }
63
- const indexes = [...chosen].sort((left, right) => left - right);
64
- const out = [];
65
- let previous = -1;
66
- for (const index of indexes) {
67
- if (index > previous + 1) {
68
- out.push({ kind: "gap", text: `… ${index - previous - 1} lines hidden · ctrl+o expand` });
69
- }
70
- const detail = all[index];
71
- if (detail !== undefined)
72
- out.push(detail);
73
- previous = index;
74
- }
75
- if (previous < all.length - 1) {
76
- out.push({
77
- kind: "gap",
78
- text: `… ${all.length - previous - 1} lines hidden · ctrl+o expand`,
79
- });
80
- }
81
- return out;
33
+ // The compact transcript is an audit of what changed, not a code excerpt.
34
+ // Context and gap rows remain in semantic state for the explicit full view.
35
+ return all.filter((detail) => detail.kind === "add" || detail.kind === "del");
82
36
  }
83
37
  function renderDetail(detail, tone, width, pal) {
84
38
  const lead = { text: " " };
package/dist/ui/theme.js CHANGED
@@ -18,7 +18,7 @@ export const STEEL = {
18
18
  },
19
19
  surface: {
20
20
  subtle: [31, 38, 47],
21
- inset: [18, 24, 31],
21
+ inset: [42, 52, 66],
22
22
  added: [22, 55, 34],
23
23
  removed: [62, 24, 27],
24
24
  attention: [62, 50, 19],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@giovannijecha/jecode",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "An owned coding agent with zero external runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "repository": {