@cubicj/codex-watch 0.2.0 → 0.3.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 +4 -0
- package/bin/codex-watch +214 -69
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,13 +4,17 @@ Live-tail the newest Codex plugin background job log for the current git repo.
|
|
|
4
4
|
|
|
5
5
|
`codex-watch` is a zero-dependency terminal companion for the [`openai/codex-plugin-cc`](https://github.com/openai/codex-plugin-cc) Claude Code plugin. When Claude Code delegates work to a background Codex job, run `codex-watch` in that repo's terminal to follow the job's log live — and it keeps following across `--resume`.
|
|
6
6
|
|
|
7
|
+

|
|
8
|
+
|
|
7
9
|
## What It Does
|
|
8
10
|
|
|
9
11
|
- Resolves the git repo from the terminal's current working directory.
|
|
10
12
|
- Follows the newest job log written by the `openai/codex-plugin-cc` Claude Code plugin.
|
|
11
13
|
- Auto-switches when a `--resume` starts a newer job id, so a resumed run is picked up without restarting.
|
|
12
14
|
- Shows the job's model, reasoning effort, sandbox mode, and write access under the header (resolved from the Codex CLI session files in `$CODEX_HOME`, default `~/.codex`).
|
|
15
|
+
- Shows a dim `Thinking...` indicator for each reasoning stretch.
|
|
13
16
|
- Colorizes assistant messages, command status, file changes, and job events.
|
|
17
|
+
- Lists each applied file with its change type and a workspace-relative path when available.
|
|
14
18
|
- Prints a completion banner such as `✓ completed · <duration>` or `✗ <status> · <duration>`.
|
|
15
19
|
- Drops duplicate captured lines.
|
|
16
20
|
|
package/bin/codex-watch
CHANGED
|
@@ -3,6 +3,7 @@ const fs = require("fs");
|
|
|
3
3
|
const path = require("path");
|
|
4
4
|
const os = require("os");
|
|
5
5
|
const childProcess = require("child_process");
|
|
6
|
+
const crypto = require("node:crypto");
|
|
6
7
|
const { StringDecoder } = require("node:string_decoder");
|
|
7
8
|
|
|
8
9
|
process.stdout.on("error", (error) => {
|
|
@@ -58,11 +59,11 @@ if (args.includes("--help") || args.includes("-h")) {
|
|
|
58
59
|
const once = args.includes("--once");
|
|
59
60
|
const repoRoot = resolveRepoRoot();
|
|
60
61
|
const slug = makeSlug(repoRoot);
|
|
62
|
+
const stateDirName = makeStateDirName(repoRoot, slug);
|
|
61
63
|
const stateRoot = process.env.CLAUDE_PLUGIN_DATA
|
|
62
64
|
? path.join(process.env.CLAUDE_PLUGIN_DATA, "state")
|
|
63
65
|
: path.join(os.homedir(), ".claude/plugins/data/codex-openai-codex/state");
|
|
64
66
|
const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
65
|
-
const stateDirPattern = new RegExp("^" + escapeRegExp(slug) + "-[0-9a-f]{6,}$");
|
|
66
67
|
|
|
67
68
|
let currentJob = null;
|
|
68
69
|
let logOffset = 0;
|
|
@@ -74,8 +75,11 @@ let scanTimer = null;
|
|
|
74
75
|
let waitingPrinted = false;
|
|
75
76
|
let completionBannerPrinted = false;
|
|
76
77
|
let sessionInfoPrinted = false;
|
|
78
|
+
let requestedSessionInfoPrinted = false;
|
|
79
|
+
let thinkingIndicatorPrinted = false;
|
|
77
80
|
let sessionRolloutPath = "";
|
|
78
81
|
let lastRolloutSearchAt = null;
|
|
82
|
+
let consumedPatchApplyEvents = new Set();
|
|
79
83
|
|
|
80
84
|
process.on("SIGINT", () => {
|
|
81
85
|
clearTimers();
|
|
@@ -92,6 +96,9 @@ if (once) {
|
|
|
92
96
|
printHeader(job);
|
|
93
97
|
printSessionInfo(job);
|
|
94
98
|
renderWholeLog(job);
|
|
99
|
+
if (isTerminalStatus(job.status)) {
|
|
100
|
+
printCompletionBanner(job, job.displayedCreatedAt);
|
|
101
|
+
}
|
|
95
102
|
process.exit(0);
|
|
96
103
|
}
|
|
97
104
|
|
|
@@ -115,8 +122,13 @@ function makeSlug(root) {
|
|
|
115
122
|
return path.basename(root).replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "workspace";
|
|
116
123
|
}
|
|
117
124
|
|
|
118
|
-
function
|
|
119
|
-
|
|
125
|
+
function makeStateDirName(root, rootSlug) {
|
|
126
|
+
let canonical = root;
|
|
127
|
+
try {
|
|
128
|
+
canonical = fs.realpathSync.native(root);
|
|
129
|
+
} catch (_error) {}
|
|
130
|
+
const hash = crypto.createHash("sha256").update(canonical).digest("hex").slice(0, 16);
|
|
131
|
+
return rootSlug + "-" + hash;
|
|
120
132
|
}
|
|
121
133
|
|
|
122
134
|
function safeReadDir(dir, options) {
|
|
@@ -144,49 +156,44 @@ function readJson(file) {
|
|
|
144
156
|
}
|
|
145
157
|
|
|
146
158
|
function findNewestJob() {
|
|
147
|
-
const
|
|
159
|
+
const jobsDir = path.join(stateRoot, stateDirName, "jobs");
|
|
160
|
+
const jobEntries = safeReadDir(jobsDir, { withFileTypes: true });
|
|
148
161
|
const candidates = [];
|
|
149
162
|
|
|
150
|
-
for (const
|
|
151
|
-
if (!
|
|
163
|
+
for (const jobEntry of jobEntries) {
|
|
164
|
+
if (!jobEntry.isFile() || !jobEntry.name.endsWith(".json")) {
|
|
152
165
|
continue;
|
|
153
166
|
}
|
|
154
167
|
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
phase: typeof meta.phase === "string" ? meta.phase : "unknown",
|
|
180
|
-
summary: typeof meta.summary === "string" ? meta.summary : "",
|
|
181
|
-
threadId: typeof meta.threadId === "string" ? meta.threadId : "",
|
|
182
|
-
write: typeof meta.write === "boolean" ? meta.write : false
|
|
183
|
-
});
|
|
184
|
-
}
|
|
168
|
+
const id = path.basename(jobEntry.name, ".json");
|
|
169
|
+
const jsonPath = path.join(jobsDir, jobEntry.name);
|
|
170
|
+
const logPath = path.join(jobsDir, id + ".log");
|
|
171
|
+
const meta = readJson(jsonPath);
|
|
172
|
+
const stat = safeStat(jsonPath);
|
|
173
|
+
const fallbackCreatedAt = stat ? stat.mtime.toISOString() : "";
|
|
174
|
+
const createdAt = typeof meta.createdAt === "string" && meta.createdAt ? meta.createdAt : fallbackCreatedAt;
|
|
175
|
+
|
|
176
|
+
candidates.push({
|
|
177
|
+
id,
|
|
178
|
+
jsonPath,
|
|
179
|
+
logPath,
|
|
180
|
+
createdAt,
|
|
181
|
+
displayedCreatedAt: typeof meta.createdAt === "string" && meta.createdAt ? meta.createdAt : createdAt,
|
|
182
|
+
startedAt: typeof meta.startedAt === "string" ? meta.startedAt : "",
|
|
183
|
+
updatedAt: typeof meta.updatedAt === "string" ? meta.updatedAt : "",
|
|
184
|
+
completedAt: typeof meta.completedAt === "string" ? meta.completedAt : "",
|
|
185
|
+
status: typeof meta.status === "string" ? meta.status : "unknown",
|
|
186
|
+
phase: typeof meta.phase === "string" ? meta.phase : "unknown",
|
|
187
|
+
summary: typeof meta.summary === "string" ? meta.summary : "",
|
|
188
|
+
threadId: typeof meta.threadId === "string" ? meta.threadId : "",
|
|
189
|
+
write: typeof meta.write === "boolean" ? meta.write : false,
|
|
190
|
+
request: meta.request && typeof meta.request === "object" ? meta.request : null
|
|
191
|
+
});
|
|
185
192
|
}
|
|
186
193
|
|
|
187
194
|
candidates.sort((a, b) => {
|
|
188
195
|
if (a.createdAt === b.createdAt) {
|
|
189
|
-
return
|
|
196
|
+
return b.id.localeCompare(a.id);
|
|
190
197
|
}
|
|
191
198
|
return a.createdAt < b.createdAt ? 1 : -1;
|
|
192
199
|
});
|
|
@@ -214,7 +221,10 @@ function scanForNewJob() {
|
|
|
214
221
|
return;
|
|
215
222
|
}
|
|
216
223
|
|
|
217
|
-
if (
|
|
224
|
+
if (
|
|
225
|
+
newest.createdAt > currentJob.createdAt ||
|
|
226
|
+
(newest.createdAt === currentJob.createdAt && newest.id.localeCompare(currentJob.id) > 0)
|
|
227
|
+
) {
|
|
218
228
|
console.log(color("dim", "──────── switched to " + newest.id + " ────────"));
|
|
219
229
|
switchToJob(newest);
|
|
220
230
|
}
|
|
@@ -229,8 +239,11 @@ function switchToJob(job) {
|
|
|
229
239
|
waitingPrinted = false;
|
|
230
240
|
completionBannerPrinted = false;
|
|
231
241
|
sessionInfoPrinted = false;
|
|
242
|
+
requestedSessionInfoPrinted = false;
|
|
243
|
+
thinkingIndicatorPrinted = false;
|
|
232
244
|
sessionRolloutPath = "";
|
|
233
245
|
lastRolloutSearchAt = null;
|
|
246
|
+
consumedPatchApplyEvents = new Set();
|
|
234
247
|
if (tailTimer) {
|
|
235
248
|
clearInterval(tailTimer);
|
|
236
249
|
}
|
|
@@ -256,20 +269,18 @@ function printHeader(job) {
|
|
|
256
269
|
}
|
|
257
270
|
|
|
258
271
|
function printSessionInfo(job) {
|
|
259
|
-
if (sessionInfoPrinted
|
|
272
|
+
if (sessionInfoPrinted) {
|
|
260
273
|
return;
|
|
261
274
|
}
|
|
262
275
|
|
|
263
|
-
if (!
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
276
|
+
if (!job.threadId) {
|
|
277
|
+
printRequestedSessionInfo(job);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!resolveSessionRolloutPath(job)) {
|
|
282
|
+
printRequestedSessionInfo(job);
|
|
283
|
+
return;
|
|
273
284
|
}
|
|
274
285
|
|
|
275
286
|
const text = readFileText(sessionRolloutPath);
|
|
@@ -295,6 +306,7 @@ function printSessionInfo(job) {
|
|
|
295
306
|
|
|
296
307
|
const payload = turnContext && turnContext.payload;
|
|
297
308
|
if (!payload || typeof payload.model !== "string" || !payload.model) {
|
|
309
|
+
printRequestedSessionInfo(job);
|
|
298
310
|
return;
|
|
299
311
|
}
|
|
300
312
|
|
|
@@ -310,6 +322,42 @@ function printSessionInfo(job) {
|
|
|
310
322
|
sessionInfoPrinted = true;
|
|
311
323
|
}
|
|
312
324
|
|
|
325
|
+
function printRequestedSessionInfo(job) {
|
|
326
|
+
const request = job.request;
|
|
327
|
+
if (
|
|
328
|
+
requestedSessionInfoPrinted ||
|
|
329
|
+
!request ||
|
|
330
|
+
typeof request.model !== "string" ||
|
|
331
|
+
!request.model
|
|
332
|
+
) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const segments = ["model " + request.model + " (requested)"];
|
|
337
|
+
if (typeof request.effort === "string" && request.effort) {
|
|
338
|
+
segments.push("effort " + request.effort + " (requested)");
|
|
339
|
+
}
|
|
340
|
+
console.log(color("dim", segments.join(" · ")));
|
|
341
|
+
requestedSessionInfoPrinted = true;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function resolveSessionRolloutPath(job) {
|
|
345
|
+
if (sessionRolloutPath) {
|
|
346
|
+
return sessionRolloutPath;
|
|
347
|
+
}
|
|
348
|
+
if (!job || !job.threadId) {
|
|
349
|
+
return "";
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const now = Date.now();
|
|
353
|
+
if (lastRolloutSearchAt !== null && now - lastRolloutSearchAt < 5000) {
|
|
354
|
+
return "";
|
|
355
|
+
}
|
|
356
|
+
lastRolloutSearchAt = now;
|
|
357
|
+
sessionRolloutPath = findRolloutFile(job.threadId);
|
|
358
|
+
return sessionRolloutPath;
|
|
359
|
+
}
|
|
360
|
+
|
|
313
361
|
function findRolloutFile(threadId) {
|
|
314
362
|
const sessionsRoot = path.join(codexHome, "sessions");
|
|
315
363
|
const years = safeReadDir(sessionsRoot, { withFileTypes: true })
|
|
@@ -371,7 +419,7 @@ function formatElapsed(createdAt, updatedAt) {
|
|
|
371
419
|
|
|
372
420
|
function renderWholeLog(job) {
|
|
373
421
|
const text = readFileText(job.logPath);
|
|
374
|
-
renderText(text);
|
|
422
|
+
renderText(text, false, job);
|
|
375
423
|
}
|
|
376
424
|
|
|
377
425
|
function readAvailableLog() {
|
|
@@ -392,7 +440,7 @@ function readAvailableLog() {
|
|
|
392
440
|
const chunk = readFileChunk(currentJob.logPath, logOffset, stat.size - logOffset);
|
|
393
441
|
if (chunk !== null) {
|
|
394
442
|
logOffset += chunk.length;
|
|
395
|
-
renderText(logDecoder.write(chunk), true);
|
|
443
|
+
renderText(logDecoder.write(chunk), true, currentJob);
|
|
396
444
|
}
|
|
397
445
|
}
|
|
398
446
|
}
|
|
@@ -401,38 +449,42 @@ function readAvailableLog() {
|
|
|
401
449
|
}
|
|
402
450
|
|
|
403
451
|
function checkJobCompletion() {
|
|
404
|
-
if (completionBannerPrinted) {
|
|
405
|
-
return true;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
452
|
let meta = null;
|
|
409
453
|
try {
|
|
410
454
|
meta = JSON.parse(fs.readFileSync(currentJob.jsonPath, "utf8"));
|
|
411
455
|
} catch (_error) {
|
|
412
|
-
return
|
|
456
|
+
return;
|
|
413
457
|
}
|
|
414
458
|
|
|
415
459
|
const status = typeof meta.status === "string" ? meta.status : "unknown";
|
|
416
460
|
currentJob.threadId = typeof meta.threadId === "string" ? meta.threadId : "";
|
|
417
461
|
currentJob.write = typeof meta.write === "boolean" ? meta.write : false;
|
|
418
462
|
currentJob.startedAt = typeof meta.startedAt === "string" ? meta.startedAt : "";
|
|
463
|
+
currentJob.request = meta.request && typeof meta.request === "object" ? meta.request : null;
|
|
419
464
|
printSessionInfo(currentJob);
|
|
465
|
+
if (completionBannerPrinted) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
420
468
|
if (!isTerminalStatus(status)) {
|
|
421
|
-
return
|
|
469
|
+
return;
|
|
422
470
|
}
|
|
423
471
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
472
|
+
printCompletionBanner(meta, currentJob.displayedCreatedAt);
|
|
473
|
+
completionBannerPrinted = true;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function printCompletionBanner(job, fallbackCreatedAt) {
|
|
477
|
+
const status = typeof job.status === "string" ? job.status : "unknown";
|
|
478
|
+
const createdAt = typeof job.createdAt === "string" && job.createdAt ? job.createdAt : fallbackCreatedAt;
|
|
479
|
+
const startedAt = typeof job.startedAt === "string" && job.startedAt ? job.startedAt : createdAt;
|
|
480
|
+
const updatedAt = typeof job.updatedAt === "string" && job.updatedAt ? job.updatedAt : "";
|
|
481
|
+
const completedAt = typeof job.completedAt === "string" && job.completedAt ? job.completedAt : updatedAt;
|
|
428
482
|
const elapsed = formatElapsed(startedAt, completedAt);
|
|
429
483
|
if (status === "completed") {
|
|
430
484
|
console.log(color("green", "✓ completed · " + elapsed));
|
|
431
485
|
} else {
|
|
432
486
|
console.log(color("red", "✗ " + status + " · " + elapsed));
|
|
433
487
|
}
|
|
434
|
-
completionBannerPrinted = true;
|
|
435
|
-
return true;
|
|
436
488
|
}
|
|
437
489
|
|
|
438
490
|
function isTerminalStatus(status) {
|
|
@@ -465,7 +517,7 @@ function readFileChunk(file, start, length) {
|
|
|
465
517
|
}
|
|
466
518
|
}
|
|
467
519
|
|
|
468
|
-
function renderText(text, keepPartial) {
|
|
520
|
+
function renderText(text, keepPartial, job) {
|
|
469
521
|
if (!text) {
|
|
470
522
|
return;
|
|
471
523
|
}
|
|
@@ -482,13 +534,14 @@ function renderText(text, keepPartial) {
|
|
|
482
534
|
}
|
|
483
535
|
|
|
484
536
|
for (const line of lines) {
|
|
485
|
-
renderLine(line);
|
|
537
|
+
renderLine(line, job);
|
|
486
538
|
}
|
|
487
539
|
}
|
|
488
540
|
|
|
489
|
-
function renderLine(line) {
|
|
490
|
-
const match = line.match(/^\[\d{4}-\d{2}-\d{2}T[^\]]
|
|
541
|
+
function renderLine(line, job) {
|
|
542
|
+
const match = line.match(/^\[(\d{4}-\d{2}-\d{2}T[^\]]*)\]\s+(.*)$/);
|
|
491
543
|
if (!match) {
|
|
544
|
+
thinkingIndicatorPrinted = false;
|
|
492
545
|
if (assistantBlock) {
|
|
493
546
|
console.log(assistantText(line));
|
|
494
547
|
} else {
|
|
@@ -497,7 +550,8 @@ function renderLine(line) {
|
|
|
497
550
|
return;
|
|
498
551
|
}
|
|
499
552
|
|
|
500
|
-
const
|
|
553
|
+
const eventTimestamp = match[1];
|
|
554
|
+
const event = match[2];
|
|
501
555
|
|
|
502
556
|
if (event === "Assistant message") {
|
|
503
557
|
assistantBlock = true;
|
|
@@ -510,6 +564,16 @@ function renderLine(line) {
|
|
|
510
564
|
return;
|
|
511
565
|
}
|
|
512
566
|
|
|
567
|
+
if (event === "Thinking.") {
|
|
568
|
+
if (!thinkingIndicatorPrinted) {
|
|
569
|
+
console.log(color("dim", "Thinking..."));
|
|
570
|
+
thinkingIndicatorPrinted = true;
|
|
571
|
+
}
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
thinkingIndicatorPrinted = false;
|
|
576
|
+
|
|
513
577
|
if (event.startsWith("Running command: ")) {
|
|
514
578
|
console.log(color("dim", color("blue", "Running command: " + truncate(event.slice("Running command: ".length)))));
|
|
515
579
|
return;
|
|
@@ -531,7 +595,13 @@ function renderLine(line) {
|
|
|
531
595
|
return;
|
|
532
596
|
}
|
|
533
597
|
|
|
534
|
-
if (
|
|
598
|
+
if (/^Applying [0-9]+ file change\(s\)\.$/.test(event)) {
|
|
599
|
+
console.log(color("yellow", event));
|
|
600
|
+
renderAppliedFiles(eventTimestamp, job);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (event.startsWith("File changes") || event === "File changes completed.") {
|
|
535
605
|
console.log(color("yellow", event));
|
|
536
606
|
return;
|
|
537
607
|
}
|
|
@@ -544,6 +614,81 @@ function renderLine(line) {
|
|
|
544
614
|
console.log(event);
|
|
545
615
|
}
|
|
546
616
|
|
|
617
|
+
function renderAppliedFiles(eventTimestamp, job) {
|
|
618
|
+
const rolloutPath = resolveSessionRolloutPath(job);
|
|
619
|
+
if (!rolloutPath) {
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const targetTime = Date.parse(eventTimestamp);
|
|
624
|
+
if (!Number.isFinite(targetTime)) {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const text = readFileText(rolloutPath);
|
|
629
|
+
let sessionCwd = process.cwd();
|
|
630
|
+
let nearest = null;
|
|
631
|
+
|
|
632
|
+
for (const [index, line] of text.split(/\r?\n/).entries()) {
|
|
633
|
+
try {
|
|
634
|
+
const entry = JSON.parse(line);
|
|
635
|
+
if (
|
|
636
|
+
entry &&
|
|
637
|
+
entry.type === "session_meta" &&
|
|
638
|
+
entry.payload &&
|
|
639
|
+
typeof entry.payload.cwd === "string" &&
|
|
640
|
+
entry.payload.cwd
|
|
641
|
+
) {
|
|
642
|
+
sessionCwd = entry.payload.cwd;
|
|
643
|
+
}
|
|
644
|
+
if (
|
|
645
|
+
consumedPatchApplyEvents.has(index) ||
|
|
646
|
+
!entry ||
|
|
647
|
+
entry.type !== "event_msg" ||
|
|
648
|
+
!entry.payload ||
|
|
649
|
+
entry.payload.type !== "patch_apply_end"
|
|
650
|
+
) {
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const patchTime = Date.parse(entry.timestamp);
|
|
655
|
+
const difference = Math.abs(patchTime - targetTime);
|
|
656
|
+
if (!Number.isFinite(difference) || difference > 2000) {
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
if (!nearest || difference < nearest.difference) {
|
|
660
|
+
nearest = { index, difference, changes: entry.payload.changes };
|
|
661
|
+
}
|
|
662
|
+
} catch (_error) {}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (!nearest) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
consumedPatchApplyEvents.add(nearest.index);
|
|
669
|
+
if (!nearest.changes || typeof nearest.changes !== "object" || Array.isArray(nearest.changes)) {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
for (const [file, change] of Object.entries(nearest.changes)) {
|
|
674
|
+
const type = change && change.type;
|
|
675
|
+
const marker = type === "update" ? "M" : type === "add" ? "A" : type === "delete" ? "D" : String(type);
|
|
676
|
+
console.log(color("dim", " " + marker + " " + renderChangedPath(file, sessionCwd)));
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function renderChangedPath(file, sessionCwd) {
|
|
681
|
+
if (!path.isAbsolute(file)) {
|
|
682
|
+
return file;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const relative = path.relative(sessionCwd, file);
|
|
686
|
+
if (relative !== ".." && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative)) {
|
|
687
|
+
return relative || ".";
|
|
688
|
+
}
|
|
689
|
+
return file;
|
|
690
|
+
}
|
|
691
|
+
|
|
547
692
|
function truncate(value) {
|
|
548
693
|
const maxLength = 140;
|
|
549
694
|
const codePoints = Array.from(value);
|