@cubicj/codex-watch 0.2.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.
Files changed (2) hide show
  1. package/bin/codex-watch +57 -47
  2. package/package.json +1 -1
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;
@@ -92,6 +93,9 @@ if (once) {
92
93
  printHeader(job);
93
94
  printSessionInfo(job);
94
95
  renderWholeLog(job);
96
+ if (isTerminalStatus(job.status)) {
97
+ printCompletionBanner(job, job.displayedCreatedAt);
98
+ }
95
99
  process.exit(0);
96
100
  }
97
101
 
@@ -115,8 +119,13 @@ function makeSlug(root) {
115
119
  return path.basename(root).replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "workspace";
116
120
  }
117
121
 
118
- function escapeRegExp(value) {
119
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
122
+ function makeStateDirName(root, rootSlug) {
123
+ let canonical = root;
124
+ try {
125
+ canonical = fs.realpathSync.native(root);
126
+ } catch (_error) {}
127
+ const hash = crypto.createHash("sha256").update(canonical).digest("hex").slice(0, 16);
128
+ return rootSlug + "-" + hash;
120
129
  }
121
130
 
122
131
  function safeReadDir(dir, options) {
@@ -144,49 +153,43 @@ function readJson(file) {
144
153
  }
145
154
 
146
155
  function findNewestJob() {
147
- const stateEntries = safeReadDir(stateRoot, { withFileTypes: true });
156
+ const jobsDir = path.join(stateRoot, stateDirName, "jobs");
157
+ const jobEntries = safeReadDir(jobsDir, { withFileTypes: true });
148
158
  const candidates = [];
149
159
 
150
- for (const entry of stateEntries) {
151
- if (!entry.isDirectory() || !stateDirPattern.test(entry.name)) {
160
+ for (const jobEntry of jobEntries) {
161
+ if (!jobEntry.isFile() || !jobEntry.name.endsWith(".json")) {
152
162
  continue;
153
163
  }
154
164
 
155
- const jobsDir = path.join(stateRoot, entry.name, "jobs");
156
- const jobEntries = safeReadDir(jobsDir, { withFileTypes: true });
157
-
158
- for (const jobEntry of jobEntries) {
159
- if (!jobEntry.isFile() || !jobEntry.name.endsWith(".json")) {
160
- continue;
161
- }
162
-
163
- const id = path.basename(jobEntry.name, ".json");
164
- const jsonPath = path.join(jobsDir, jobEntry.name);
165
- const logPath = path.join(jobsDir, id + ".log");
166
- const meta = readJson(jsonPath);
167
- const stat = safeStat(jsonPath);
168
- const fallbackCreatedAt = stat ? stat.mtime.toISOString() : "";
169
- const createdAt = typeof meta.createdAt === "string" && meta.createdAt ? meta.createdAt : fallbackCreatedAt;
170
-
171
- candidates.push({
172
- id,
173
- jsonPath,
174
- logPath,
175
- createdAt,
176
- displayedCreatedAt: typeof meta.createdAt === "string" && meta.createdAt ? meta.createdAt : createdAt,
177
- startedAt: typeof meta.startedAt === "string" ? meta.startedAt : "",
178
- status: typeof meta.status === "string" ? meta.status : "unknown",
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
- }
165
+ const id = path.basename(jobEntry.name, ".json");
166
+ const jsonPath = path.join(jobsDir, jobEntry.name);
167
+ const logPath = path.join(jobsDir, id + ".log");
168
+ const meta = readJson(jsonPath);
169
+ const stat = safeStat(jsonPath);
170
+ const fallbackCreatedAt = stat ? stat.mtime.toISOString() : "";
171
+ const createdAt = typeof meta.createdAt === "string" && meta.createdAt ? meta.createdAt : fallbackCreatedAt;
172
+
173
+ candidates.push({
174
+ id,
175
+ jsonPath,
176
+ logPath,
177
+ createdAt,
178
+ displayedCreatedAt: typeof meta.createdAt === "string" && meta.createdAt ? meta.createdAt : createdAt,
179
+ startedAt: typeof meta.startedAt === "string" ? meta.startedAt : "",
180
+ updatedAt: typeof meta.updatedAt === "string" ? meta.updatedAt : "",
181
+ completedAt: typeof meta.completedAt === "string" ? meta.completedAt : "",
182
+ status: typeof meta.status === "string" ? meta.status : "unknown",
183
+ phase: typeof meta.phase === "string" ? meta.phase : "unknown",
184
+ summary: typeof meta.summary === "string" ? meta.summary : "",
185
+ threadId: typeof meta.threadId === "string" ? meta.threadId : "",
186
+ write: typeof meta.write === "boolean" ? meta.write : false
187
+ });
185
188
  }
186
189
 
187
190
  candidates.sort((a, b) => {
188
191
  if (a.createdAt === b.createdAt) {
189
- return a.id.localeCompare(b.id);
192
+ return b.id.localeCompare(a.id);
190
193
  }
191
194
  return a.createdAt < b.createdAt ? 1 : -1;
192
195
  });
@@ -214,7 +217,10 @@ function scanForNewJob() {
214
217
  return;
215
218
  }
216
219
 
217
- if (newest.createdAt > currentJob.createdAt) {
220
+ if (
221
+ newest.createdAt > currentJob.createdAt ||
222
+ (newest.createdAt === currentJob.createdAt && newest.id.localeCompare(currentJob.id) > 0)
223
+ ) {
218
224
  console.log(color("dim", "──────── switched to " + newest.id + " ────────"));
219
225
  switchToJob(newest);
220
226
  }
@@ -402,14 +408,14 @@ function readAvailableLog() {
402
408
 
403
409
  function checkJobCompletion() {
404
410
  if (completionBannerPrinted) {
405
- return true;
411
+ return;
406
412
  }
407
413
 
408
414
  let meta = null;
409
415
  try {
410
416
  meta = JSON.parse(fs.readFileSync(currentJob.jsonPath, "utf8"));
411
417
  } catch (_error) {
412
- return false;
418
+ return;
413
419
  }
414
420
 
415
421
  const status = typeof meta.status === "string" ? meta.status : "unknown";
@@ -418,21 +424,25 @@ function checkJobCompletion() {
418
424
  currentJob.startedAt = typeof meta.startedAt === "string" ? meta.startedAt : "";
419
425
  printSessionInfo(currentJob);
420
426
  if (!isTerminalStatus(status)) {
421
- return true;
427
+ return;
422
428
  }
423
429
 
424
- const createdAt = typeof meta.createdAt === "string" && meta.createdAt ? meta.createdAt : currentJob.displayedCreatedAt;
425
- const startedAt = typeof meta.startedAt === "string" && meta.startedAt ? meta.startedAt : createdAt;
426
- const updatedAt = typeof meta.updatedAt === "string" && meta.updatedAt ? meta.updatedAt : "";
427
- const completedAt = typeof meta.completedAt === "string" && meta.completedAt ? meta.completedAt : updatedAt;
430
+ printCompletionBanner(meta, currentJob.displayedCreatedAt);
431
+ completionBannerPrinted = true;
432
+ }
433
+
434
+ function printCompletionBanner(job, fallbackCreatedAt) {
435
+ const status = typeof job.status === "string" ? job.status : "unknown";
436
+ const createdAt = typeof job.createdAt === "string" && job.createdAt ? job.createdAt : fallbackCreatedAt;
437
+ const startedAt = typeof job.startedAt === "string" && job.startedAt ? job.startedAt : createdAt;
438
+ const updatedAt = typeof job.updatedAt === "string" && job.updatedAt ? job.updatedAt : "";
439
+ const completedAt = typeof job.completedAt === "string" && job.completedAt ? job.completedAt : updatedAt;
428
440
  const elapsed = formatElapsed(startedAt, completedAt);
429
441
  if (status === "completed") {
430
442
  console.log(color("green", "✓ completed · " + elapsed));
431
443
  } else {
432
444
  console.log(color("red", "✗ " + status + " · " + elapsed));
433
445
  }
434
- completionBannerPrinted = true;
435
- return true;
436
446
  }
437
447
 
438
448
  function isTerminalStatus(status) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cubicj/codex-watch",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Live-tail the newest Codex plugin background job log for the current git repo.",
5
5
  "license": "MIT",
6
6
  "repository": {