get-tbd 0.1.24 → 0.1.25

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 CHANGED
@@ -279,7 +279,7 @@ npm install -g get-tbd@latest
279
279
  ### Setup
280
280
 
281
281
  ```bash
282
- # Fresh project (--prefix is REQUIRED—2-8 alphabetic chars, e.g. myapp-a1b2)
282
+ # Fresh project (--prefix is REQUIRED—a short alphabetic name used as an issue ID prefix, e.g. myapp → issues like myapp-a1b2)
283
283
  tbd setup --auto --prefix=myapp
284
284
 
285
285
  # Joining an existing tbd project (no prefix needed—reads existing config)
@@ -299,7 +299,7 @@ tbd setup --from-beads
299
299
  **First contributor:**
300
300
  ```bash
301
301
  npm install -g get-tbd@latest
302
- tbd setup --auto --prefix=myproject
302
+ tbd setup --auto --prefix=proj # Short alphabetic prefix for issue IDs
303
303
  git add .tbd/ .claude/ && git commit -m "Initialize tbd"
304
304
  git push
305
305
  ```
package/dist/bin.mjs CHANGED
@@ -14033,7 +14033,7 @@ function serializeIssue(issue) {
14033
14033
  * Package version, derived from git at build time.
14034
14034
  * Format: X.Y.Z for releases, X.Y.Z-dev.N.hash for dev builds.
14035
14035
  */
14036
- const VERSION$1 = "0.1.24";
14036
+ const VERSION$1 = "0.1.25";
14037
14037
 
14038
14038
  //#endregion
14039
14039
  //#region src/cli/lib/version.ts
@@ -99751,28 +99751,32 @@ async function listIssues(baseDir) {
99751
99751
  return [];
99752
99752
  }
99753
99753
  const mdFiles = files.filter((f) => f.endsWith(".md"));
99754
- const fileContents = await Promise.all(mdFiles.map(async (file) => {
99755
- const filePath = join(issuesDir, file);
99756
- try {
99757
- return {
99758
- file,
99759
- content: await readFile(filePath, "utf-8")
99760
- };
99761
- } catch {
99762
- return {
99763
- file,
99764
- content: null
99765
- };
99766
- }
99767
- }));
99754
+ const BATCH_SIZE = 200;
99768
99755
  const issues = [];
99769
- for (const { file, content } of fileContents) {
99770
- if (content === null) continue;
99771
- try {
99772
- const issue = parseIssue(content);
99773
- issues.push(issue);
99774
- } catch (error) {
99775
- console.warn(`Skipping invalid issue file: ${file}`, error);
99756
+ for (let i = 0; i < mdFiles.length; i += BATCH_SIZE) {
99757
+ const batch = mdFiles.slice(i, i + BATCH_SIZE);
99758
+ const fileContents = await Promise.all(batch.map(async (file) => {
99759
+ const filePath = join(issuesDir, file);
99760
+ try {
99761
+ return {
99762
+ file,
99763
+ content: await readFile(filePath, "utf-8")
99764
+ };
99765
+ } catch {
99766
+ return {
99767
+ file,
99768
+ content: null
99769
+ };
99770
+ }
99771
+ }));
99772
+ for (const { file, content } of fileContents) {
99773
+ if (content === null) continue;
99774
+ try {
99775
+ const issue = parseIssue(content);
99776
+ issues.push(issue);
99777
+ } catch (error) {
99778
+ console.warn(`Skipping invalid issue file: ${file}`, error);
99779
+ }
99776
99780
  }
99777
99781
  }
99778
99782
  return issues;
@@ -99814,30 +99818,44 @@ async function listIssues(baseDir) {
99814
99818
  * crashed and break the lock. This is a heuristic — safe when the critical
99815
99819
  * section is short-lived (sub-second for file I/O).
99816
99820
  *
99817
- * ## Degraded mode
99821
+ * ## Failure on timeout
99822
+ *
99823
+ * If the lock cannot be acquired within the timeout, a LockAcquisitionError is
99824
+ * thrown. This prevents the dangerous "degraded mode" where the critical section
99825
+ * runs without mutual exclusion, which can cause data loss (e.g., lost ID
99826
+ * mappings during concurrent `tbd create`).
99818
99827
  *
99819
- * If the lock cannot be acquired within the timeout (e.g., due to a stuck
99820
- * lockfile that isn't old enough to break), the critical section runs anyway.
99821
- * Callers should design their critical sections to be safe without the lock
99822
- * (e.g., using read-merge-write for append-only data).
99828
+ * IMPORTANT: `timeoutMs` must be greater than `staleMs` so stale locks from
99829
+ * crashed processes are always detected and broken before the timeout expires.
99823
99830
  */
99824
- const DEFAULT_TIMEOUT_MS = 2e3;
99831
+ const DEFAULT_TIMEOUT_MS = 1e4;
99825
99832
  const DEFAULT_POLL_MS = 50;
99826
99833
  const DEFAULT_STALE_MS = 5e3;
99827
99834
  /**
99835
+ * Error thrown when the lock cannot be acquired within the timeout.
99836
+ */
99837
+ var LockAcquisitionError = class extends Error {
99838
+ constructor(lockPath, timeoutMs) {
99839
+ super(`Failed to acquire lock at ${lockPath} within ${timeoutMs}ms. Another process may be holding the lock. If this persists, delete the lock directory manually and retry.`);
99840
+ this.name = "LockAcquisitionError";
99841
+ }
99842
+ };
99843
+ /**
99828
99844
  * Execute `fn` while holding a lockfile.
99829
99845
  *
99830
99846
  * The lock is a directory at `lockPath` (typically `<target-file>.lock`).
99831
99847
  * Concurrent callers will wait up to `timeoutMs` for the lock, polling
99832
99848
  * every `pollMs`. Stale locks older than `staleMs` are broken automatically.
99833
99849
  *
99834
- * If the lock cannot be acquired, `fn` is still executed (degraded mode).
99835
- * This ensures a stuck lockfile never permanently blocks the CLI.
99850
+ * If the lock cannot be acquired within the timeout, a LockAcquisitionError
99851
+ * is thrown. This ensures mutual exclusion is never silently bypassed, which
99852
+ * prevents data loss from concurrent writes.
99836
99853
  *
99837
99854
  * @param lockPath - Path to use as the lock directory (e.g., "/path/to/ids.yml.lock")
99838
99855
  * @param fn - Critical section to execute under the lock
99839
99856
  * @param options - Timing parameters for lock acquisition
99840
99857
  * @returns The return value of `fn`
99858
+ * @throws LockAcquisitionError if the lock cannot be acquired within the timeout
99841
99859
  *
99842
99860
  * @example
99843
99861
  * ```ts
@@ -99873,10 +99891,11 @@ async function withLockfile(lockPath, fn, options) {
99873
99891
  }
99874
99892
  await new Promise((resolve) => setTimeout(resolve, pollMs));
99875
99893
  }
99894
+ if (!acquired) throw new LockAcquisitionError(lockPath, timeoutMs);
99876
99895
  try {
99877
99896
  return await fn();
99878
99897
  } finally {
99879
- if (acquired) try {
99898
+ try {
99880
99899
  await rmdir(lockPath);
99881
99900
  } catch {}
99882
99901
  }
@@ -100033,8 +100052,8 @@ async function loadIdMapping(baseDir) {
100033
100052
  * commands run in parallel.
100034
100053
  *
100035
100054
  * The merge is safe because ID mappings are append-only — entries are never
100036
- * intentionally removed. Even if the lock acquisition fails (degraded mode),
100037
- * the read-merge-write provides a fallback that preserves entries from other writers.
100055
+ * intentionally removed. If the lock cannot be acquired within the timeout,
100056
+ * a LockAcquisitionError is thrown rather than proceeding without protection.
100038
100057
  */
100039
100058
  async function saveIdMapping(baseDir, mapping) {
100040
100059
  const filePath = getMappingPath(baseDir);
@@ -104603,11 +104622,18 @@ var DoctorHandler = class extends BaseCommand {
104603
104622
  healthChecks.push(await this.checkIdMappingDuplicates(options.fix));
104604
104623
  healthChecks.push(await this.checkTempFiles(options.fix));
104605
104624
  healthChecks.push(this.checkIssueValidity(this.issues));
104625
+ healthChecks.push(await this.checkWorktree(options.fix));
104626
+ const dataLocationResult = await this.checkDataLocation(options.fix);
104627
+ healthChecks.push(dataLocationResult);
104628
+ if (dataLocationResult.status === "ok" && dataLocationResult.message?.includes("migrated")) {
104629
+ this.dataSyncDir = await resolveDataSyncDir(this.cwd);
104630
+ try {
104631
+ this.issues = await listIssues(this.dataSyncDir);
104632
+ } catch {}
104633
+ }
104606
104634
  const parsedMaxHistory = options.maxHistory ? parseInt(options.maxHistory, 10) : 50;
104607
104635
  const maxHistory = Number.isNaN(parsedMaxHistory) || parsedMaxHistory < 0 ? 50 : parsedMaxHistory;
104608
104636
  healthChecks.push(await this.checkMissingMappings(options.fix, maxHistory));
104609
- healthChecks.push(await this.checkWorktree(options.fix));
104610
- healthChecks.push(await this.checkDataLocation(options.fix));
104611
104637
  healthChecks.push(await this.checkLocalSyncBranch());
104612
104638
  healthChecks.push(await this.checkRemoteSyncBranch());
104613
104639
  healthChecks.push(await this.checkLocalVsRemoteData());