codetime-cli 0.7.0 → 0.7.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/codetime.mjs +154 -40
  2. package/package.json +1 -1
package/bin/codetime.mjs CHANGED
@@ -19,10 +19,13 @@ __export(fs_exports, {
19
19
  parseJsonFile: () => parseJsonFile,
20
20
  pathExists: () => pathExists,
21
21
  readJsonIfExists: () => readJsonIfExists,
22
+ readJsonIfExistsTolerant: () => readJsonIfExistsTolerant,
22
23
  readTextIfExists: () => readTextIfExists,
24
+ writeFileAtomic: () => writeFileAtomic,
23
25
  writeGeneratedFile: () => writeGeneratedFile
24
26
  });
25
- import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
27
+ import { randomBytes } from "node:crypto";
28
+ import { mkdir, open, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
26
29
  import path from "node:path";
27
30
  async function pathExists(filePath) {
28
31
  try {
@@ -56,6 +59,59 @@ function parseJsonFile(filePath, text) {
56
59
  throw new Error(`Could not parse JSON file ${filePath}: ${error.message}`);
57
60
  }
58
61
  }
62
+ async function readJsonIfExistsTolerant(filePath, onCorrupt) {
63
+ const text = await readTextIfExists(filePath);
64
+ if (text === null) {
65
+ return null;
66
+ }
67
+ try {
68
+ return JSON.parse(text);
69
+ } catch (error) {
70
+ onCorrupt?.(error);
71
+ return null;
72
+ }
73
+ }
74
+ async function writeFileAtomic(filePath, data, options = {}) {
75
+ const dir = path.dirname(filePath);
76
+ await mkdir(dir, { recursive: true });
77
+ const tmpPath = path.join(
78
+ dir,
79
+ `.${path.basename(filePath)}.${process.pid}.${Date.now()}.${randomBytes(6).toString("hex")}.tmp`
80
+ );
81
+ let handle;
82
+ try {
83
+ handle = await open(tmpPath, "wx", options.mode ?? 438);
84
+ await handle.writeFile(data, "utf8");
85
+ if (options.mode !== void 0) {
86
+ await handle.chmod(options.mode);
87
+ }
88
+ await handle.close();
89
+ handle = void 0;
90
+ await renameWithRetry(tmpPath, filePath);
91
+ } catch (error) {
92
+ if (handle) {
93
+ await handle.close().catch(() => {
94
+ });
95
+ }
96
+ await rm(tmpPath, { force: true }).catch(() => {
97
+ });
98
+ throw error;
99
+ }
100
+ }
101
+ async function renameWithRetry(from, to, attempts = 5) {
102
+ for (let i = 0; ; i++) {
103
+ try {
104
+ await rename(from, to);
105
+ return;
106
+ } catch (error) {
107
+ const code = error.code;
108
+ if (i >= attempts - 1 || code !== "EPERM" && code !== "EACCES" && code !== "EBUSY") {
109
+ throw error;
110
+ }
111
+ await new Promise((resolve) => setTimeout(resolve, 10 * (i + 1)));
112
+ }
113
+ }
114
+ }
59
115
  async function writeGeneratedFile(filePath, content, { dryRun, force, onWrite }) {
60
116
  if (dryRun) {
61
117
  onWrite(`Would write ${filePath}`);
@@ -152,7 +208,7 @@ var init_fs = __esm({
152
208
 
153
209
  // src/cli.ts
154
210
  import { spawn } from "node:child_process";
155
- import { mkdir as mkdir3, rm, stat as stat6, writeFile as writeFile2 } from "node:fs/promises";
211
+ import { rm as rm2, stat as stat6 } from "node:fs/promises";
156
212
  import os3 from "node:os";
157
213
  import path13 from "node:path";
158
214
  import { fileURLToPath } from "node:url";
@@ -974,7 +1030,7 @@ import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
974
1030
  import path2 from "node:path";
975
1031
 
976
1032
  // src/lib/constants.ts
977
- var PACKAGE_VERSION = true ? "0.7.0" : "0.1.1";
1033
+ var PACKAGE_VERSION = true ? "0.7.1" : "0.1.1";
978
1034
  var DEFAULT_API_URL = "https://codetime.dev";
979
1035
  var DEFAULT_BACKFILL_BATCH_SIZE = 50;
980
1036
  var DEFAULT_BACKFILL_BATCH_BYTES = 800 * 1024;
@@ -4516,7 +4572,7 @@ async function installEntry(entry, options) {
4516
4572
  });
4517
4573
  }
4518
4574
  async function mergeHooksJson(filePath, content, { dryRun, force, onWrite }) {
4519
- const { mkdir: mkdir4, writeFile: writeFile3 } = await import("node:fs/promises");
4575
+ const { mkdir: mkdir3, writeFile: writeFile2 } = await import("node:fs/promises");
4520
4576
  const pathMod = await import("node:path");
4521
4577
  if (dryRun) {
4522
4578
  onWrite(`Would merge ${filePath}`);
@@ -4537,8 +4593,8 @@ async function mergeHooksJson(filePath, content, { dryRun, force, onWrite }) {
4537
4593
  onWrite(`Already installed ${filePath}`);
4538
4594
  return;
4539
4595
  }
4540
- await mkdir4(pathMod.dirname(filePath), { recursive: true });
4541
- await writeFile3(filePath, nextText, "utf8");
4596
+ await mkdir3(pathMod.dirname(filePath), { recursive: true });
4597
+ await writeFile2(filePath, nextText, "utf8");
4542
4598
  onWrite(`Installed ${filePath}`);
4543
4599
  }
4544
4600
  function mergeHookObjects(existing, addition) {
@@ -4576,7 +4632,7 @@ function hookCommandFromGroup(group) {
4576
4632
 
4577
4633
  // src/lib/config.ts
4578
4634
  import { randomUUID } from "node:crypto";
4579
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4635
+ import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, unlinkSync, writeSync } from "node:fs";
4580
4636
  import { homedir, hostname } from "node:os";
4581
4637
  import path11 from "node:path";
4582
4638
  function configDir(home = homedir()) {
@@ -4600,30 +4656,87 @@ function readConfig(home = homedir()) {
4600
4656
  return {};
4601
4657
  }
4602
4658
  }
4603
- function writeConfig(config, home = homedir()) {
4604
- const dir = configDir(home);
4605
- if (!existsSync(dir)) {
4606
- mkdirSync(dir, { recursive: true });
4659
+ var atomicTmpCounter = 0;
4660
+ function writeFileAtomicSync(filePath, data, mode) {
4661
+ const dir = path11.dirname(filePath);
4662
+ mkdirSync(dir, { recursive: true });
4663
+ const tmp = path11.join(dir, `.${path11.basename(filePath)}.${process.pid}.${Date.now()}.${(atomicTmpCounter++).toString(36)}.tmp`);
4664
+ let fd;
4665
+ try {
4666
+ fd = openSync(tmp, "wx", mode);
4667
+ writeSync(fd, data);
4668
+ closeSync(fd);
4669
+ fd = void 0;
4670
+ chmodSync(tmp, mode);
4671
+ renameSyncWithRetry(tmp, filePath);
4672
+ } catch (error) {
4673
+ if (fd !== void 0) {
4674
+ try {
4675
+ closeSync(fd);
4676
+ } catch {
4677
+ }
4678
+ }
4679
+ try {
4680
+ unlinkSync(tmp);
4681
+ } catch {
4682
+ }
4683
+ throw error;
4684
+ }
4685
+ }
4686
+ function renameSyncWithRetry(from, to, attempts = 5) {
4687
+ for (let i = 0; ; i++) {
4688
+ try {
4689
+ renameSync(from, to);
4690
+ return;
4691
+ } catch (error) {
4692
+ const code = error.code;
4693
+ if (i >= attempts - 1 || code !== "EPERM" && code !== "EACCES" && code !== "EBUSY") {
4694
+ throw error;
4695
+ }
4696
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10 * (i + 1));
4697
+ }
4607
4698
  }
4608
- writeFileSync(configPath(home), `${JSON.stringify(config, null, 2)}
4609
- `, { mode: 384 });
4699
+ }
4700
+ function writeConfig(config, home = homedir()) {
4701
+ writeFileAtomicSync(configPath(home), `${JSON.stringify(config, null, 2)}
4702
+ `, 384);
4610
4703
  }
4611
4704
  function ensureLocalMachineId(home = homedir()) {
4612
4705
  const file = machineIdPath(home);
4613
- if (existsSync(file)) {
4614
- const value = readFileSync(file, "utf8").trim();
4615
- if (value.length > 0) {
4616
- return value;
4617
- }
4706
+ const existing = readMachineId(file);
4707
+ if (existing) {
4708
+ return existing;
4618
4709
  }
4710
+ mkdirSync(configDir(home), { recursive: true });
4619
4711
  const id = randomUUID();
4620
- const dir = configDir(home);
4621
- if (!existsSync(dir)) {
4622
- mkdirSync(dir, { recursive: true });
4712
+ try {
4713
+ const fd = openSync(file, "wx", 384);
4714
+ try {
4715
+ writeSync(fd, `${id}
4716
+ `);
4717
+ } finally {
4718
+ closeSync(fd);
4719
+ }
4720
+ return id;
4721
+ } catch (error) {
4722
+ if (error.code !== "EEXIST") {
4723
+ throw error;
4724
+ }
4725
+ const peer = readMachineId(file);
4726
+ if (peer) {
4727
+ return peer;
4728
+ }
4729
+ writeFileAtomicSync(file, `${id}
4730
+ `, 384);
4731
+ return id;
4623
4732
  }
4624
- writeFileSync(file, `${id}
4625
- `, { mode: 384 });
4626
- return id;
4733
+ }
4734
+ function readMachineId(file) {
4735
+ if (!existsSync(file)) {
4736
+ return null;
4737
+ }
4738
+ const value = readFileSync(file, "utf8").trim();
4739
+ return value.length > 0 ? value : null;
4627
4740
  }
4628
4741
  function defaultMachineName() {
4629
4742
  return hostname();
@@ -4633,7 +4746,7 @@ function defaultMachineName() {
4633
4746
  init_fs();
4634
4747
 
4635
4748
  // src/lib/logger.ts
4636
- import { appendFile, mkdir as mkdir2, rename, stat as stat5 } from "node:fs/promises";
4749
+ import { appendFile, mkdir as mkdir2, rename as rename2, stat as stat5 } from "node:fs/promises";
4637
4750
  import { homedir as homedir2 } from "node:os";
4638
4751
  import path12 from "node:path";
4639
4752
  var MAX_BYTES = 1 * 1024 * 1024;
@@ -4653,7 +4766,7 @@ async function rotateIfNeeded(file) {
4653
4766
  try {
4654
4767
  const info = await stat5(file);
4655
4768
  if (info.size > MAX_BYTES) {
4656
- await rename(file, `${file}.1`).catch(() => {
4769
+ await rename2(file, `${file}.1`).catch(() => {
4657
4770
  });
4658
4771
  }
4659
4772
  } catch {
@@ -5487,7 +5600,7 @@ async function importBackfillPlan(plan, options, ctx, registry) {
5487
5600
  }
5488
5601
  async function purgeForcedSources(sourceDefs, home, options, ctx) {
5489
5602
  try {
5490
- await rm(backfillIncrementalStatePath(home), { force: true });
5603
+ await rm2(backfillIncrementalStatePath(home), { force: true });
5491
5604
  } catch (error) {
5492
5605
  debug(ctx, `Failed to clear backfill watermark: ${error.message}
5493
5606
  `);
@@ -5685,7 +5798,11 @@ function syncLocalTriggerLockPath(home) {
5685
5798
  }
5686
5799
  async function readBackfillIncrementalState(home, ctx) {
5687
5800
  const statePath = backfillIncrementalStatePath(home);
5688
- const state = await readJsonIfExists(statePath);
5801
+ const state = await readJsonIfExistsTolerant(
5802
+ statePath,
5803
+ ctx ? (error) => debug(ctx, `backfill-state unreadable at ${statePath}; dropping watermarks and re-importing (${error.message})
5804
+ `) : void 0
5805
+ );
5689
5806
  if (state === null) {
5690
5807
  return { version: BACKFILL_STATE_SCHEMA_VERSION, sources: {} };
5691
5808
  }
@@ -5721,12 +5838,11 @@ async function updateBackfillIncrementalState(home, state, selectedFilesBySource
5721
5838
  state.sources[source] = { watermarkTs: latest };
5722
5839
  }
5723
5840
  const statePath = backfillIncrementalStatePath(home);
5724
- await mkdir3(path13.dirname(statePath), { recursive: true });
5725
- await writeFile2(statePath, `${JSON.stringify(state, null, 2)}
5726
- `, "utf8");
5841
+ await writeFileAtomic(statePath, `${JSON.stringify(state, null, 2)}
5842
+ `);
5727
5843
  }
5728
5844
  async function readSyncLocalTriggerState(statePath) {
5729
- const state = await readJsonIfExists(statePath);
5845
+ const state = await readJsonIfExistsTolerant(statePath);
5730
5846
  if (!isPlainObject(state)) {
5731
5847
  return { version: 1 };
5732
5848
  }
@@ -5749,12 +5865,11 @@ async function readSyncLocalTriggerState(statePath) {
5749
5865
  return nextState;
5750
5866
  }
5751
5867
  async function writeSyncLocalTriggerState(statePath, state) {
5752
- await mkdir3(path13.dirname(statePath), { recursive: true });
5753
- await writeFile2(statePath, `${JSON.stringify(state, null, 2)}
5754
- `, "utf8");
5868
+ await writeFileAtomic(statePath, `${JSON.stringify(state, null, 2)}
5869
+ `);
5755
5870
  }
5756
5871
  async function readSyncLocalLock(lockPath) {
5757
- const lock = await readJsonIfExists(lockPath);
5872
+ const lock = await readJsonIfExistsTolerant(lockPath);
5758
5873
  if (!isPlainObject(lock)) {
5759
5874
  return void 0;
5760
5875
  }
@@ -5764,13 +5879,12 @@ async function readSyncLocalLock(lockPath) {
5764
5879
  return { pid: lock.pid, startedAt: lock.startedAt };
5765
5880
  }
5766
5881
  async function writeSyncLocalLock(lockPath, lock) {
5767
- await mkdir3(path13.dirname(lockPath), { recursive: true });
5768
- await writeFile2(lockPath, `${JSON.stringify(lock, null, 2)}
5769
- `, "utf8");
5882
+ await writeFileAtomic(lockPath, `${JSON.stringify(lock, null, 2)}
5883
+ `);
5770
5884
  }
5771
5885
  async function clearSyncLocalLock(lockPath) {
5772
5886
  try {
5773
- await rm(lockPath, { force: true });
5887
+ await rm2(lockPath, { force: true });
5774
5888
  } catch (error) {
5775
5889
  if (error.code !== "ENOENT") {
5776
5890
  throw error;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "codetime-cli",
3
3
  "type": "module",
4
- "version": "0.7.0",
4
+ "version": "0.7.1",
5
5
  "description": "codetime CLI — install AI-agent hooks (Claude Code, Codex, OpenCode, Pi) and report activity to codetime.dev.",
6
6
  "license": "MIT",
7
7
  "publishConfig": {