get-tbd 0.1.26 → 0.1.27

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/dist/cli.mjs CHANGED
@@ -1,9 +1,10 @@
1
- import { _ as IssueKind, n as AtticEntrySchema, t as ATTIC_ENTRY_FIELD_ORDER, y as IssueStatus } from "./schemas-BQYmDnkv.mjs";
2
- import { a as insertAfterFrontmatter, c as noopLogger, i as serializeIssue, n as parseIssue, o as parseMarkdown, r as parseMarkdownWithFrontmatter, s as stripFrontmatter, t as VERSION$1 } from "./src-DQcOQnFp.mjs";
1
+ import { S as IssueTitle, b as IssueSchema, g as ISSUE_TITLE_MAX_LENGTH, n as AtticEntrySchema, t as ATTIC_ENTRY_FIELD_ORDER, x as IssueStatus, y as IssueKind } from "./schemas-C8mOQykE.mjs";
2
+ import { a as insertAfterFrontmatter, c as noopLogger, i as serializeIssue, n as parseIssue, o as parseMarkdown, r as parseMarkdownWithFrontmatter, s as stripFrontmatter, t as VERSION$1 } from "./src-BIE27KDA.mjs";
3
3
  import { a as parseYamlWithConflictDetection, d as PAGINATION_LINE_THRESHOLD, f as PARENT_CONTEXT_MAX_LINES, l as comparisonChain, n as detectDuplicateYamlKeys, o as sortKeys, s as stringifyYaml, u as ordering } from "./yaml-utils-BPy991by.mjs";
4
- import { A as isValidWorkspaceName, C as TBD_SHORTCUTS_STANDARD, D as WORKTREE_DIR, E as WORKSPACES_DIR, M as resolveDataSyncDir, O as WORKTREE_DIR_NAME, S as TBD_GUIDELINES_DIR, T as TBD_TEMPLATES_DIR, _ as DEFAULT_SHORTCUT_PATHS, a as isInitialized, b as TBD_DIR, c as readConfigWithMigration, d as writeConfig, g as DEFAULT_GUIDELINES_PATHS, h as DATA_SYNC_DIR_NAME, i as initConfig, j as resolveAtticDir, k as getWorkspaceDir, l as readLocalState, m as DATA_SYNC_DIR, n as findTbdRoot, o as markWelcomeSeen, p as CHARS_PER_TOKEN, r as hasSeenWelcome, s as readConfig, u as updateLocalState, v as DEFAULT_TEMPLATE_PATHS, w as TBD_SHORTCUTS_SYSTEM, x as TBD_DOCS_DIR, y as SYNC_BRANCH } from "./config-b20Kf5pW.mjs";
5
- import { _ as formatDisplayId, a as hasShortId, b as normalizeIssueId, c as parseIdMappingFromYaml, d as resolveToInternalId, f as saveIdMapping, g as formatDebugId, h as extractUlidFromInternalId, i as generateUniqueShortId, l as reconcileMappings, m as extractShortId, o as loadIdMapping, p as extractPrefix, s as mergeIdMappings, t as addIdMapping, u as resolveIdMappingConflicts, v as generateInternalId, x as validateIssueId, y as makeInternalId } from "./id-mapping-BtBwq5nG.mjs";
4
+ import { A as isValidWorkspaceName, C as TBD_SHORTCUTS_STANDARD, D as WORKTREE_DIR, E as WORKSPACES_DIR, M as resolveDataSyncDir, O as WORKTREE_DIR_NAME, S as TBD_GUIDELINES_DIR, T as TBD_TEMPLATES_DIR, _ as DEFAULT_SHORTCUT_PATHS, a as isInitialized, b as TBD_DIR, c as readConfigWithMigration, d as writeConfig, g as DEFAULT_GUIDELINES_PATHS, h as DATA_SYNC_DIR_NAME, i as initConfig, j as resolveAtticDir, k as getWorkspaceDir, l as readLocalState, m as DATA_SYNC_DIR, n as findTbdRoot, o as markWelcomeSeen, p as CHARS_PER_TOKEN, r as hasSeenWelcome, s as readConfig, u as updateLocalState, v as DEFAULT_TEMPLATE_PATHS, w as TBD_SHORTCUTS_SYSTEM, x as TBD_DOCS_DIR, y as SYNC_BRANCH } from "./config-B38rbI9u.mjs";
5
+ import { _ as formatDisplayId, a as hasShortId, b as normalizeIssueId, c as parseIdMappingFromYaml, d as resolveToInternalId, f as saveIdMapping, g as formatDebugId, h as extractUlidFromInternalId, i as generateUniqueShortId, l as reconcileMappings, m as extractShortId, o as loadIdMapping, p as extractPrefix, s as mergeIdMappings, t as addIdMapping, u as resolveIdMappingConflicts, v as generateInternalId, x as validateIssueId, y as makeInternalId } from "./id-mapping-CqrrLgeX.mjs";
6
6
  import { createRequire } from "node:module";
7
+ import { ZodError } from "zod";
7
8
  import matter from "gray-matter";
8
9
  import { parse } from "yaml";
9
10
  import { Command } from "commander";
@@ -1754,7 +1755,7 @@ async function migrateDataToWorktree(baseDir, removeSource = false) {
1754
1755
  for (const file of issueFiles) await cp(join(wrongIssuesPath, file), join(correctIssuesPath, file));
1755
1756
  for (const file of mappingFiles) if (file === "ids.yml") {
1756
1757
  const { readFile } = await import("node:fs/promises");
1757
- const { loadIdMapping, mergeIdMappings, saveIdMapping, resolveIdMappingConflicts } = await import("./id-mapping-BA_xn516.mjs");
1758
+ const { loadIdMapping, mergeIdMappings, saveIdMapping, resolveIdMappingConflicts } = await import("./id-mapping-Ctfl_nc1.mjs");
1758
1759
  const sourceMapping = resolveIdMappingConflicts(await readFile(join(wrongMappingsPath, file), "utf-8"));
1759
1760
  let targetMapping;
1760
1761
  try {
@@ -1883,6 +1884,29 @@ const initCommand = new Command("init").description("Initialize tbd in a git rep
1883
1884
  await new InitHandler(command).run(options);
1884
1885
  });
1885
1886
 
1887
+ //#endregion
1888
+ //#region src/utils/zod-error-utils.ts
1889
+ /**
1890
+ * Helpers for rendering Zod errors without relying on object inspection.
1891
+ */
1892
+ /**
1893
+ * Format a ZodError as concise path-qualified messages for CLI output.
1894
+ */
1895
+ function formatZodError(error) {
1896
+ const messages = error.issues.map((issue) => {
1897
+ return `${issue.path.length > 0 ? issue.path.join(".") : "<root>"}: ${issue.message}`;
1898
+ });
1899
+ return messages.length > 0 ? messages.join("; ") : error.message;
1900
+ }
1901
+ /**
1902
+ * Format unknown thrown values as safe strings for warnings and diagnostics.
1903
+ */
1904
+ function formatUnknownError(error) {
1905
+ if (error instanceof ZodError) return formatZodError(error);
1906
+ if (error instanceof Error) return error.message;
1907
+ return String(error);
1908
+ }
1909
+
1886
1910
  //#endregion
1887
1911
  //#region src/file/storage.ts
1888
1912
  /**
@@ -1894,6 +1918,10 @@ const initCommand = new Command("init").description("Initialize tbd in a git rep
1894
1918
  * See: tbd-design.md §3.2 Storage Layer
1895
1919
  */
1896
1920
  /**
1921
+ * Maximum issue files read concurrently to avoid exhausting file descriptors in large repos.
1922
+ */
1923
+ const ISSUE_READ_BATCH_SIZE = 200;
1924
+ /**
1897
1925
  * Get the path to an issue file.
1898
1926
  */
1899
1927
  function getIssuePath(baseDir, id) {
@@ -1911,7 +1939,8 @@ async function readIssue(baseDir, id) {
1911
1939
  * Uses atomic write to prevent corruption.
1912
1940
  */
1913
1941
  async function writeIssue(baseDir, issue) {
1914
- await writeFile(getIssuePath(baseDir, issue.id), serializeIssue(issue));
1942
+ const validIssue = IssueSchema.parse(issue);
1943
+ await writeFile(getIssuePath(baseDir, validIssue.id), serializeIssue(validIssue));
1915
1944
  }
1916
1945
  /**
1917
1946
  * List all issues in the worktree.
@@ -1919,7 +1948,8 @@ async function writeIssue(baseDir, issue) {
1919
1948
  *
1920
1949
  * Uses parallel file reading for better performance with many issues.
1921
1950
  */
1922
- async function listIssues(baseDir) {
1951
+ async function listIssues(baseDir, options = {}) {
1952
+ const warnOnInvalid = options.warnOnInvalid ?? true;
1923
1953
  const issuesDir = join(baseDir, "issues");
1924
1954
  let files;
1925
1955
  try {
@@ -1928,10 +1958,9 @@ async function listIssues(baseDir) {
1928
1958
  return [];
1929
1959
  }
1930
1960
  const mdFiles = files.filter((f) => f.endsWith(".md"));
1931
- const BATCH_SIZE = 200;
1932
1961
  const issues = [];
1933
- for (let i = 0; i < mdFiles.length; i += BATCH_SIZE) {
1934
- const batch = mdFiles.slice(i, i + BATCH_SIZE);
1962
+ for (let i = 0; i < mdFiles.length; i += ISSUE_READ_BATCH_SIZE) {
1963
+ const batch = mdFiles.slice(i, i + ISSUE_READ_BATCH_SIZE);
1935
1964
  const fileContents = await Promise.all(batch.map(async (file) => {
1936
1965
  const filePath = join(issuesDir, file);
1937
1966
  try {
@@ -1939,25 +1968,38 @@ async function listIssues(baseDir) {
1939
1968
  file,
1940
1969
  content: await readFile(filePath, "utf-8")
1941
1970
  };
1942
- } catch {
1971
+ } catch (error) {
1943
1972
  return {
1944
1973
  file,
1945
- content: null
1974
+ error: formatUnknownError(error)
1946
1975
  };
1947
1976
  }
1948
1977
  }));
1949
- for (const { file, content } of fileContents) {
1950
- if (content === null) continue;
1978
+ for (const result of fileContents) {
1979
+ if ("error" in result) {
1980
+ reportInvalidIssueFile({
1981
+ file: result.file,
1982
+ reason: `failed to read file: ${result.error}`
1983
+ }, warnOnInvalid, options.onInvalidIssue);
1984
+ continue;
1985
+ }
1951
1986
  try {
1952
- const issue = parseIssue(content);
1987
+ const issue = parseIssue(result.content);
1953
1988
  issues.push(issue);
1954
1989
  } catch (error) {
1955
- console.warn(`Skipping invalid issue file: ${file}`, error);
1990
+ reportInvalidIssueFile({
1991
+ file: result.file,
1992
+ reason: formatUnknownError(error)
1993
+ }, warnOnInvalid, options.onInvalidIssue);
1956
1994
  }
1957
1995
  }
1958
1996
  }
1959
1997
  return issues;
1960
1998
  }
1999
+ function reportInvalidIssueFile(invalidIssue, warnOnInvalid, onInvalidIssue) {
2000
+ onInvalidIssue?.(invalidIssue);
2001
+ if (warnOnInvalid) console.warn(`Skipping invalid issue file: ${invalidIssue.file}: ${invalidIssue.reason}`);
2002
+ }
1961
2003
 
1962
2004
  //#endregion
1963
2005
  //#region src/lib/priority.ts
@@ -2150,6 +2192,22 @@ async function resolveAndValidatePath(inputPath, projectRoot, cwd) {
2150
2192
  return resolved;
2151
2193
  }
2152
2194
 
2195
+ //#endregion
2196
+ //#region src/cli/lib/issue-input-validation.ts
2197
+ /**
2198
+ * CLI validation helpers for user-provided issue fields.
2199
+ */
2200
+ /**
2201
+ * Validate a CLI-provided issue title with actionable user-facing errors.
2202
+ */
2203
+ function validateIssueTitle(title, options) {
2204
+ if (options.rejectBlank ? title.trim().length === 0 : title.length === 0) throw new ValidationError(options.emptyMessage);
2205
+ if (title.length > ISSUE_TITLE_MAX_LENGTH) throw new ValidationError(`Title is too long (${title.length} chars, max ${ISSUE_TITLE_MAX_LENGTH}). Move detail into the description body.`);
2206
+ const result = IssueTitle.safeParse(title);
2207
+ if (!result.success) throw new ValidationError(`Invalid title: ${formatZodError(result.error)}`);
2208
+ return title;
2209
+ }
2210
+
2153
2211
  //#endregion
2154
2212
  //#region src/cli/commands/create.ts
2155
2213
  /**
@@ -2161,6 +2219,10 @@ var CreateHandler = class extends BaseCommand {
2161
2219
  async run(title, options) {
2162
2220
  const tbdRoot = await requireInit();
2163
2221
  if (!title && !options.fromFile) throw new ValidationError("Title is required. Use: tbd create \"Issue title\"");
2222
+ const validatedTitle = title === void 0 ? void 0 : validateIssueTitle(title, {
2223
+ emptyMessage: "Title is required. Use: tbd create \"Issue title\"",
2224
+ rejectBlank: true
2225
+ });
2164
2226
  const kind = this.parseKind(options.type ?? "task");
2165
2227
  const priority = this.validatePriority(options.priority ?? "2");
2166
2228
  let description = options.description;
@@ -2177,7 +2239,7 @@ var CreateHandler = class extends BaseCommand {
2177
2239
  throw new ValidationError(getPathErrorMessage(error));
2178
2240
  }
2179
2241
  if (this.checkDryRun("Would create issue", {
2180
- title,
2242
+ title: validatedTitle,
2181
2243
  kind,
2182
2244
  priority,
2183
2245
  spec: specPath,
@@ -2209,7 +2271,7 @@ var CreateHandler = class extends BaseCommand {
2209
2271
  type: "is",
2210
2272
  id,
2211
2273
  version: 1,
2212
- title,
2274
+ title: validatedTitle,
2213
2275
  kind,
2214
2276
  status: "open",
2215
2277
  priority,
@@ -2241,9 +2303,9 @@ var CreateHandler = class extends BaseCommand {
2241
2303
  this.output.data({
2242
2304
  id: displayId,
2243
2305
  internalId: id,
2244
- title
2306
+ title: validatedTitle
2245
2307
  }, () => {
2246
- this.output.success(`Created ${displayId}: ${title}`);
2308
+ this.output.success(`Created ${displayId}: ${validatedTitle}`);
2247
2309
  });
2248
2310
  }
2249
2311
  parseKind(value) {
@@ -3059,7 +3121,10 @@ var UpdateHandler = class extends BaseCommand {
3059
3121
  }
3060
3122
  try {
3061
3123
  const { frontmatter, description, notes } = parseMarkdownWithFrontmatter(content);
3062
- if (typeof frontmatter.title === "string") updates.title = frontmatter.title;
3124
+ if (typeof frontmatter.title === "string") updates.title = validateIssueTitle(frontmatter.title, {
3125
+ emptyMessage: "Title cannot be empty",
3126
+ rejectBlank: true
3127
+ });
3063
3128
  if (typeof frontmatter.status === "string") {
3064
3129
  const result = IssueStatus.safeParse(frontmatter.status);
3065
3130
  if (result.success) updates.status = result.data;
@@ -3090,10 +3155,10 @@ var UpdateHandler = class extends BaseCommand {
3090
3155
  }
3091
3156
  return updates;
3092
3157
  }
3093
- if (options.title !== void 0) {
3094
- if (!options.title.trim()) throw new ValidationError("Title cannot be empty");
3095
- updates.title = options.title;
3096
- }
3158
+ if (options.title !== void 0) updates.title = validateIssueTitle(options.title, {
3159
+ emptyMessage: "Title cannot be empty",
3160
+ rejectBlank: true
3161
+ });
3097
3162
  if (options.status) {
3098
3163
  const result = IssueStatus.safeParse(options.status);
3099
3164
  if (!result.success) throw new ValidationError(`Invalid status: ${options.status}`);
@@ -6302,6 +6367,7 @@ var DoctorHandler = class extends BaseCommand {
6302
6367
  cwd = "";
6303
6368
  config = null;
6304
6369
  issues = [];
6370
+ invalidIssueFiles = [];
6305
6371
  async run(options) {
6306
6372
  const tbdRoot = await requireInit();
6307
6373
  this.cwd = tbdRoot;
@@ -6310,7 +6376,11 @@ var DoctorHandler = class extends BaseCommand {
6310
6376
  this.config = await readConfig(this.cwd);
6311
6377
  } catch {}
6312
6378
  try {
6313
- this.issues = await listIssues(this.dataSyncDir);
6379
+ this.invalidIssueFiles = [];
6380
+ this.issues = await listIssues(this.dataSyncDir, {
6381
+ warnOnInvalid: false,
6382
+ onInvalidIssue: (invalidIssue) => this.invalidIssueFiles.push(invalidIssue)
6383
+ });
6314
6384
  } catch {}
6315
6385
  const statusInfo = await this.gatherStatusInfo();
6316
6386
  const statsInfo = await this.gatherStatsInfo();
@@ -6323,14 +6393,18 @@ var DoctorHandler = class extends BaseCommand {
6323
6393
  healthChecks.push(await this.checkIdMappingConflicts(options.fix));
6324
6394
  healthChecks.push(await this.checkIdMappingDuplicates(options.fix));
6325
6395
  healthChecks.push(await this.checkTempFiles(options.fix));
6326
- healthChecks.push(this.checkIssueValidity(this.issues));
6396
+ healthChecks.push(this.checkIssueValidity(this.issues, this.invalidIssueFiles));
6327
6397
  healthChecks.push(await this.checkWorktree(options.fix));
6328
6398
  const dataLocationResult = await this.checkDataLocation(options.fix);
6329
6399
  healthChecks.push(dataLocationResult);
6330
6400
  if (dataLocationResult.status === "ok" && dataLocationResult.message?.includes("migrated")) {
6331
6401
  this.dataSyncDir = await resolveDataSyncDir(this.cwd);
6332
6402
  try {
6333
- this.issues = await listIssues(this.dataSyncDir);
6403
+ this.invalidIssueFiles = [];
6404
+ this.issues = await listIssues(this.dataSyncDir, {
6405
+ warnOnInvalid: false,
6406
+ onInvalidIssue: (invalidIssue) => this.invalidIssueFiles.push(invalidIssue)
6407
+ });
6334
6408
  } catch {}
6335
6409
  }
6336
6410
  const parsedMaxHistory = options.maxHistory ? parseInt(options.maxHistory, 10) : 50;
@@ -6561,7 +6635,7 @@ var DoctorHandler = class extends BaseCommand {
6561
6635
  status: "ok"
6562
6636
  };
6563
6637
  if (fix && !this.checkDryRun("Resolve merge conflicts in ids.yml")) try {
6564
- const { resolveIdMappingConflicts, saveIdMapping } = await import("./id-mapping-BA_xn516.mjs");
6638
+ const { resolveIdMappingConflicts, saveIdMapping } = await import("./id-mapping-Ctfl_nc1.mjs");
6565
6639
  const resolved = resolveIdMappingConflicts(content);
6566
6640
  await saveIdMapping(this.dataSyncDir, resolved);
6567
6641
  return {
@@ -6610,7 +6684,7 @@ var DoctorHandler = class extends BaseCommand {
6610
6684
  status: "ok"
6611
6685
  };
6612
6686
  if (fix && !this.checkDryRun("Fix duplicate ID mapping keys")) try {
6613
- const { loadIdMapping, saveIdMapping } = await import("./id-mapping-BA_xn516.mjs");
6687
+ const { loadIdMapping, saveIdMapping } = await import("./id-mapping-Ctfl_nc1.mjs");
6614
6688
  const mapping = await loadIdMapping(this.dataSyncDir);
6615
6689
  await saveIdMapping(this.dataSyncDir, mapping);
6616
6690
  return {
@@ -6673,8 +6747,12 @@ var DoctorHandler = class extends BaseCommand {
6673
6747
  suggestion: "Run: tbd doctor --fix"
6674
6748
  };
6675
6749
  }
6676
- checkIssueValidity(issues) {
6750
+ checkIssueValidity(issues, invalidIssueFiles) {
6677
6751
  const invalid = [];
6752
+ for (const invalidIssueFile of invalidIssueFiles) invalid.push({
6753
+ id: invalidIssueFile.file,
6754
+ reason: invalidIssueFile.reason
6755
+ });
6678
6756
  for (const issue of issues) {
6679
6757
  const issueId = issue.id ?? "unknown";
6680
6758
  if (!issue.id) {
@@ -6724,7 +6802,8 @@ var DoctorHandler = class extends BaseCommand {
6724
6802
  return {
6725
6803
  name: "Issue validity",
6726
6804
  status: "error",
6727
- message: `${invalid.length} invalid issue(s)`,
6805
+ message: `${invalid.length} invalid issue file(s)`,
6806
+ path: join(CONFIG_DIR, "issues"),
6728
6807
  details: invalid.map((i) => `${i.id}: ${i.reason}`),
6729
6808
  suggestion: "Manually fix or delete invalid issue files"
6730
6809
  };
@@ -6744,7 +6823,7 @@ var DoctorHandler = class extends BaseCommand {
6744
6823
  name: "ID mapping coverage",
6745
6824
  status: "ok"
6746
6825
  };
6747
- const { loadIdMapping, saveIdMapping, reconcileMappings } = await import("./id-mapping-BA_xn516.mjs");
6826
+ const { loadIdMapping, saveIdMapping, reconcileMappings } = await import("./id-mapping-Ctfl_nc1.mjs");
6748
6827
  const mapping = await loadIdMapping(this.dataSyncDir);
6749
6828
  const missingIds = [];
6750
6829
  for (const issue of this.issues) {
@@ -6756,10 +6835,10 @@ var DoctorHandler = class extends BaseCommand {
6756
6835
  status: "ok"
6757
6836
  };
6758
6837
  if (fix && !this.checkDryRun("Create missing ID mappings")) {
6759
- const { parseIdMappingFromYaml, mergeIdMappings } = await import("./id-mapping-BA_xn516.mjs");
6838
+ const { parseIdMappingFromYaml, mergeIdMappings } = await import("./id-mapping-Ctfl_nc1.mjs");
6760
6839
  let historicalMapping;
6761
6840
  try {
6762
- const syncBranch = (await import("./config-BZte2m3w.mjs").then((m) => m.readConfig(this.cwd))).sync.branch;
6841
+ const syncBranch = (await import("./config-C0ITTrtc.mjs").then((m) => m.readConfig(this.cwd))).sync.branch;
6763
6842
  const logArgs = ["log", "--format=%H"];
6764
6843
  if (maxHistory > 0) logArgs.push(`-${maxHistory}`);
6765
6844
  logArgs.push(syncBranch, "--", `${DATA_SYNC_DIR}/mappings/ids.yml`);