get-tbd 0.1.21 → 0.1.23
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 +17 -19
- package/dist/bin.mjs +181 -12
- package/dist/bin.mjs.map +1 -1
- package/dist/cli.mjs +110 -644
- package/dist/cli.mjs.map +1 -1
- package/dist/config-CB1tcqTZ.mjs +3 -0
- package/dist/config-CmEAGaxz.mjs +637 -0
- package/dist/config-CmEAGaxz.mjs.map +1 -0
- package/dist/docs/README.md +17 -19
- package/dist/docs/guidelines/bun-monorepo-patterns.md +816 -80
- package/dist/docs/guidelines/pnpm-monorepo-patterns.md +586 -16
- package/dist/docs/guidelines/python-cli-patterns.md +2 -2
- package/dist/docs/guidelines/tbd-sync-troubleshooting.md +27 -0
- package/dist/docs/guidelines/typescript-cli-tool-rules.md +465 -196
- package/dist/docs/tbd-design.md +86 -46
- package/dist/docs/tbd-docs.md +0 -6
- package/dist/id-mapping-0-R0X8zb.mjs +3 -0
- package/dist/{id-mapping-CD5c_ZVA.mjs → id-mapping-JGow6Jk4.mjs} +57 -3
- package/dist/{id-mapping-CD5c_ZVA.mjs.map → id-mapping-JGow6Jk4.mjs.map} +1 -1
- package/dist/index.d.mts +6 -0
- package/dist/index.mjs +2 -2
- package/dist/{src-BjMRpmMh.mjs → src-7qUDeWJf.mjs} +3 -3
- package/dist/{src-BjMRpmMh.mjs.map → src-7qUDeWJf.mjs.map} +1 -1
- package/dist/tbd +181 -12
- package/dist/{yaml-utils-x_kr2IId.mjs → yaml-utils-U7l9hhkh.mjs} +7 -1
- package/dist/yaml-utils-U7l9hhkh.mjs.map +1 -0
- package/package.json +4 -4
- package/dist/id-mapping-BqSnxlxk.mjs +0 -3
- package/dist/yaml-utils-x_kr2IId.mjs.map +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
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-
|
|
3
|
-
import { _ as
|
|
1
|
+
import { D as IssueKind, a as stringifyYaml, c as ordering, d as ATTIC_ENTRY_FIELD_ORDER, f as AtticEntrySchema, i as sortKeys, k as IssueStatus, l as PAGINATION_LINE_THRESHOLD, r as parseYamlWithConflictDetection, s as comparisonChain, t as detectDuplicateYamlKeys, u as PARENT_CONTEXT_MAX_LINES } from "./yaml-utils-U7l9hhkh.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-7qUDeWJf.mjs";
|
|
3
|
+
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-CmEAGaxz.mjs";
|
|
4
|
+
import { _ as generateInternalId, a as hasShortId, b as validateIssueId, c as parseIdMappingFromYaml, d as saveIdMapping, f as extractPrefix, g as formatDisplayId, h as formatDebugId, i as generateUniqueShortId, l as reconcileMappings, m as extractUlidFromInternalId, o as loadIdMapping, p as extractShortId, s as mergeIdMappings, t as addIdMapping, u as resolveToInternalId, v as makeInternalId, y as normalizeIssueId } from "./id-mapping-JGow6Jk4.mjs";
|
|
4
5
|
import { createRequire } from "node:module";
|
|
5
6
|
import matter from "gray-matter";
|
|
6
7
|
import { parse } from "yaml";
|
|
@@ -10,7 +11,7 @@ import { marked } from "marked";
|
|
|
10
11
|
import { markedTerminal } from "marked-terminal";
|
|
11
12
|
import { execFile, execSync, spawn, spawnSync } from "node:child_process";
|
|
12
13
|
import { access, chmod, cp, mkdir, readFile, readdir, rename, rm, stat, unlink } from "node:fs/promises";
|
|
13
|
-
import { basename, dirname, isAbsolute, join, normalize,
|
|
14
|
+
import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
14
15
|
import { writeFile } from "atomically";
|
|
15
16
|
import { homedir } from "node:os";
|
|
16
17
|
import { promisify } from "node:util";
|
|
@@ -52,15 +53,12 @@ const VERSION = getVersion();
|
|
|
52
53
|
*/
|
|
53
54
|
function getCommandContext(command) {
|
|
54
55
|
const opts = command.optsWithGlobals();
|
|
55
|
-
const isCI = Boolean(process.env.CI);
|
|
56
56
|
return {
|
|
57
57
|
dryRun: opts.dryRun ?? false,
|
|
58
58
|
verbose: opts.verbose ?? false,
|
|
59
59
|
quiet: opts.quiet ?? false,
|
|
60
60
|
json: opts.json ?? false,
|
|
61
61
|
color: opts.color ?? "auto",
|
|
62
|
-
nonInteractive: opts.nonInteractive ?? (!process.stdin.isTTY || isCI),
|
|
63
|
-
yes: opts.yes ?? false,
|
|
64
62
|
sync: opts.sync !== false,
|
|
65
63
|
debug: opts.debug ?? false
|
|
66
64
|
};
|
|
@@ -550,634 +548,6 @@ var OutputManager = class {
|
|
|
550
548
|
}
|
|
551
549
|
};
|
|
552
550
|
|
|
553
|
-
//#endregion
|
|
554
|
-
//#region src/lib/paths.ts
|
|
555
|
-
/**
|
|
556
|
-
* Centralized path constants for tbd.
|
|
557
|
-
*
|
|
558
|
-
* Directory structure (per spec):
|
|
559
|
-
*
|
|
560
|
-
* On main/dev branches:
|
|
561
|
-
* .tbd/
|
|
562
|
-
* Committed to the repo:
|
|
563
|
-
* config.yml - Project configuration
|
|
564
|
-
* .gitignore - Controls what's gitignored below
|
|
565
|
-
* workspaces/ - Persistent state (outbox, named workspaces)
|
|
566
|
-
* Gitignored (local only):
|
|
567
|
-
* state.yml - Local state
|
|
568
|
-
* docs/ - Installed documentation (regenerated on setup)
|
|
569
|
-
* data-sync-worktree/ - Hidden worktree checkout of tbd-sync branch
|
|
570
|
-
* .tbd/data-sync/ - issues/, mappings/, attic/, meta.yml
|
|
571
|
-
*
|
|
572
|
-
* On tbd-sync branch:
|
|
573
|
-
* .tbd/
|
|
574
|
-
* data-sync/
|
|
575
|
-
* issues/
|
|
576
|
-
* mappings/
|
|
577
|
-
* attic/
|
|
578
|
-
* meta.yml
|
|
579
|
-
*/
|
|
580
|
-
/** The tbd configuration directory on main branch */
|
|
581
|
-
const TBD_DIR = ".tbd";
|
|
582
|
-
/** The config file path */
|
|
583
|
-
const CONFIG_FILE = join(TBD_DIR, "config.yml");
|
|
584
|
-
/** The local state file (gitignored) */
|
|
585
|
-
const STATE_FILE = join(TBD_DIR, "state.yml");
|
|
586
|
-
/** The worktree directory name */
|
|
587
|
-
const WORKTREE_DIR_NAME = "data-sync-worktree";
|
|
588
|
-
/** The worktree path (gitignored) */
|
|
589
|
-
const WORKTREE_DIR = join(TBD_DIR, WORKTREE_DIR_NAME);
|
|
590
|
-
/** The data directory name on the sync branch */
|
|
591
|
-
const DATA_SYNC_DIR_NAME = "data-sync";
|
|
592
|
-
/**
|
|
593
|
-
* The base directory for synced data.
|
|
594
|
-
*
|
|
595
|
-
* NOTE: This is currently pointing directly to .tbd/data-sync/ which is WRONG
|
|
596
|
-
* per the spec. The correct path should be via the worktree:
|
|
597
|
-
* .tbd/data-sync-worktree/.tbd/data-sync/
|
|
598
|
-
*
|
|
599
|
-
* TODO(tbd-208): Update this to use the worktree path once worktree
|
|
600
|
-
* management is implemented.
|
|
601
|
-
*/
|
|
602
|
-
const DATA_SYNC_DIR = join(TBD_DIR, DATA_SYNC_DIR_NAME);
|
|
603
|
-
/**
|
|
604
|
-
* The correct path for synced data via worktree (per spec).
|
|
605
|
-
* Use this once worktree management is implemented.
|
|
606
|
-
*/
|
|
607
|
-
const DATA_SYNC_DIR_VIA_WORKTREE = join(WORKTREE_DIR, TBD_DIR, DATA_SYNC_DIR_NAME);
|
|
608
|
-
/** Issues directory */
|
|
609
|
-
const ISSUES_DIR = join(DATA_SYNC_DIR, "issues");
|
|
610
|
-
/** Mappings directory */
|
|
611
|
-
const MAPPINGS_DIR = join(DATA_SYNC_DIR, "mappings");
|
|
612
|
-
/** Attic directory for conflict resolution */
|
|
613
|
-
const ATTIC_DIR = join(DATA_SYNC_DIR, "attic");
|
|
614
|
-
/** Meta file for schema version */
|
|
615
|
-
const META_FILE = join(DATA_SYNC_DIR, "meta.yml");
|
|
616
|
-
/** The sync branch name */
|
|
617
|
-
const SYNC_BRANCH = "tbd-sync";
|
|
618
|
-
/** The workspaces directory name within .tbd/ */
|
|
619
|
-
const WORKSPACES_DIR_NAME = "workspaces";
|
|
620
|
-
/** Full path to workspaces directory: .tbd/workspaces/ */
|
|
621
|
-
const WORKSPACES_DIR = join(TBD_DIR, WORKSPACES_DIR_NAME);
|
|
622
|
-
/**
|
|
623
|
-
* Get the path to a named workspace directory.
|
|
624
|
-
*
|
|
625
|
-
* Workspaces are stored at: .tbd/workspaces/{name}/
|
|
626
|
-
*
|
|
627
|
-
* @param workspaceName - The name of the workspace (e.g., 'outbox', 'my-feature')
|
|
628
|
-
* @returns Path to the workspace directory
|
|
629
|
-
*/
|
|
630
|
-
function getWorkspaceDir(workspaceName) {
|
|
631
|
-
return join(WORKSPACES_DIR, workspaceName);
|
|
632
|
-
}
|
|
633
|
-
/**
|
|
634
|
-
* Validate a workspace name.
|
|
635
|
-
*
|
|
636
|
-
* Valid workspace names:
|
|
637
|
-
* - Lowercase alphanumeric characters
|
|
638
|
-
* - Hyphens and underscores allowed
|
|
639
|
-
* - Must not be empty
|
|
640
|
-
* - Must not contain path separators or dots at start
|
|
641
|
-
*
|
|
642
|
-
* @param name - The workspace name to validate
|
|
643
|
-
* @returns true if the name is valid
|
|
644
|
-
*/
|
|
645
|
-
function isValidWorkspaceName(name) {
|
|
646
|
-
if (!name || name.length === 0) return false;
|
|
647
|
-
if (name.startsWith(".")) return false;
|
|
648
|
-
return /^[a-z0-9][a-z0-9_-]*$/.test(name);
|
|
649
|
-
}
|
|
650
|
-
/** Docs directory name within .tbd/ */
|
|
651
|
-
const DOCS_DIR = "docs";
|
|
652
|
-
/** Shortcuts directory name within docs/ */
|
|
653
|
-
const SHORTCUTS_DIR = "shortcuts";
|
|
654
|
-
/** System shortcuts directory name (core docs like skill-baseline.md) */
|
|
655
|
-
const SYSTEM_DIR = "system";
|
|
656
|
-
/** Standard shortcuts directory name (workflow shortcuts) */
|
|
657
|
-
const STANDARD_DIR = "standard";
|
|
658
|
-
/** Guidelines directory name (coding rules and best practices) */
|
|
659
|
-
const GUIDELINES_DIR = "guidelines";
|
|
660
|
-
/** Templates directory name (document templates) */
|
|
661
|
-
const TEMPLATES_DIR = "templates";
|
|
662
|
-
/** Full path to docs directory: .tbd/docs/ */
|
|
663
|
-
const TBD_DOCS_DIR = join(TBD_DIR, DOCS_DIR);
|
|
664
|
-
/** Full path to shortcuts directory: .tbd/docs/shortcuts/ */
|
|
665
|
-
const TBD_SHORTCUTS_DIR = join(TBD_DOCS_DIR, SHORTCUTS_DIR);
|
|
666
|
-
/** Full path to system shortcuts: .tbd/docs/shortcuts/system/ */
|
|
667
|
-
const TBD_SHORTCUTS_SYSTEM = join(TBD_SHORTCUTS_DIR, SYSTEM_DIR);
|
|
668
|
-
/** Full path to standard shortcuts: .tbd/docs/shortcuts/standard/ */
|
|
669
|
-
const TBD_SHORTCUTS_STANDARD = join(TBD_SHORTCUTS_DIR, STANDARD_DIR);
|
|
670
|
-
/** Full path to guidelines: .tbd/docs/guidelines/ (top-level, not under shortcuts) */
|
|
671
|
-
const TBD_GUIDELINES_DIR = join(TBD_DOCS_DIR, GUIDELINES_DIR);
|
|
672
|
-
/** Full path to templates: .tbd/docs/templates/ (top-level, not under shortcuts) */
|
|
673
|
-
const TBD_TEMPLATES_DIR = join(TBD_DOCS_DIR, TEMPLATES_DIR);
|
|
674
|
-
/** Built-in docs source paths (relative to package docs/) */
|
|
675
|
-
const BUILTIN_SHORTCUTS_SYSTEM = join(SHORTCUTS_DIR, SYSTEM_DIR);
|
|
676
|
-
const BUILTIN_SHORTCUTS_STANDARD = join(SHORTCUTS_DIR, STANDARD_DIR);
|
|
677
|
-
/**
|
|
678
|
-
* Default shortcut lookup paths (searched in order, relative to tbd root).
|
|
679
|
-
* Earlier paths take precedence over later paths.
|
|
680
|
-
* Note: Guidelines and templates are now separate top-level directories.
|
|
681
|
-
*/
|
|
682
|
-
const DEFAULT_SHORTCUT_PATHS = [TBD_SHORTCUTS_SYSTEM, TBD_SHORTCUTS_STANDARD];
|
|
683
|
-
/**
|
|
684
|
-
* Default guidelines lookup paths (relative to tbd root).
|
|
685
|
-
*/
|
|
686
|
-
const DEFAULT_GUIDELINES_PATHS = [TBD_GUIDELINES_DIR];
|
|
687
|
-
/**
|
|
688
|
-
* Default template lookup paths (relative to tbd root).
|
|
689
|
-
*/
|
|
690
|
-
const DEFAULT_TEMPLATE_PATHS = [TBD_TEMPLATES_DIR];
|
|
691
|
-
/**
|
|
692
|
-
* Error thrown when worktree is missing and fallback is not allowed.
|
|
693
|
-
* Defined inline to avoid circular dependency with errors.ts.
|
|
694
|
-
*/
|
|
695
|
-
var WorktreeMissingError$1 = class extends Error {
|
|
696
|
-
constructor(message = "Worktree not found at .tbd/data-sync-worktree/. Run 'tbd doctor --fix' to repair.") {
|
|
697
|
-
super(message);
|
|
698
|
-
this.name = "WorktreeMissingError";
|
|
699
|
-
}
|
|
700
|
-
};
|
|
701
|
-
/**
|
|
702
|
-
* Cache for resolved data sync directory.
|
|
703
|
-
* Reset when baseDir changes.
|
|
704
|
-
*/
|
|
705
|
-
let _resolvedDataSyncDir = null;
|
|
706
|
-
let _resolvedBaseDir = null;
|
|
707
|
-
let _resolvedAllowFallback = null;
|
|
708
|
-
/**
|
|
709
|
-
* Resolve the actual data sync directory path.
|
|
710
|
-
*
|
|
711
|
-
* This function detects whether we're running with a git worktree
|
|
712
|
-
* (production) or in a test environment without worktree.
|
|
713
|
-
*
|
|
714
|
-
* Order of preference:
|
|
715
|
-
* 1. Worktree path if worktree exists: .tbd/data-sync-worktree/.tbd/data-sync/
|
|
716
|
-
* 2. Direct path as fallback (only if allowFallback: true)
|
|
717
|
-
*
|
|
718
|
-
* @param baseDir - The tbd root directory (from requireInit or findTbdRoot)
|
|
719
|
-
* @param options - Options for path resolution
|
|
720
|
-
* @returns Resolved data sync directory path
|
|
721
|
-
* @throws WorktreeMissingError if worktree missing and allowFallback is false
|
|
722
|
-
*
|
|
723
|
-
* See: plan-2026-01-28-sync-worktree-recovery-and-hardening.md
|
|
724
|
-
*/
|
|
725
|
-
async function resolveDataSyncDir(baseDir, options) {
|
|
726
|
-
const allowFallback = options?.allowFallback ?? true;
|
|
727
|
-
if (_resolvedDataSyncDir && _resolvedBaseDir === baseDir && _resolvedAllowFallback === allowFallback) return _resolvedDataSyncDir;
|
|
728
|
-
const worktreePath = join(baseDir, DATA_SYNC_DIR_VIA_WORKTREE);
|
|
729
|
-
const directPath = join(baseDir, DATA_SYNC_DIR);
|
|
730
|
-
try {
|
|
731
|
-
await access(worktreePath);
|
|
732
|
-
_resolvedDataSyncDir = worktreePath;
|
|
733
|
-
_resolvedBaseDir = baseDir;
|
|
734
|
-
_resolvedAllowFallback = allowFallback;
|
|
735
|
-
return worktreePath;
|
|
736
|
-
} catch {
|
|
737
|
-
if (!allowFallback) throw new WorktreeMissingError$1();
|
|
738
|
-
if (process.env.DEBUG || process.env.TBD_DEBUG) console.warn("[tbd:paths] resolveDataSyncDir: worktree not found, falling back to direct path");
|
|
739
|
-
_resolvedDataSyncDir = directPath;
|
|
740
|
-
_resolvedBaseDir = baseDir;
|
|
741
|
-
_resolvedAllowFallback = allowFallback;
|
|
742
|
-
return directPath;
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
/**
|
|
746
|
-
* Resolve attic directory path.
|
|
747
|
-
*/
|
|
748
|
-
async function resolveAtticDir(baseDir, options) {
|
|
749
|
-
return join(await resolveDataSyncDir(baseDir, options), "attic");
|
|
750
|
-
}
|
|
751
|
-
/**
|
|
752
|
-
* Characters per token ratio for estimating token counts.
|
|
753
|
-
*
|
|
754
|
-
* Based on research of OpenAI (tiktoken) and Claude tokenizers:
|
|
755
|
-
* - Pure English prose: ~4-5 chars/token
|
|
756
|
-
* - Code and symbols: ~3 chars/token
|
|
757
|
-
* - Mixed markdown/code docs: ~3.5 chars/token
|
|
758
|
-
*
|
|
759
|
-
* We use 3.5 as our docs are markdown with code examples.
|
|
760
|
-
* This provides ~15-20% accuracy, sufficient for cost estimation.
|
|
761
|
-
*/
|
|
762
|
-
const CHARS_PER_TOKEN = 3.5;
|
|
763
|
-
|
|
764
|
-
//#endregion
|
|
765
|
-
//#region src/lib/tbd-format.ts
|
|
766
|
-
/**
|
|
767
|
-
* tbd Directory Format Versioning
|
|
768
|
-
* ================================
|
|
769
|
-
*
|
|
770
|
-
* This file is the SINGLE SOURCE OF TRUTH for .tbd/ directory format versions.
|
|
771
|
-
*
|
|
772
|
-
* WHEN TO BUMP THE FORMAT VERSION:
|
|
773
|
-
* - Bump when changes REQUIRE migration (deleting files, changing formats, moving files)
|
|
774
|
-
* - **Bump when changing config schema** (adding, removing, or modifying fields)
|
|
775
|
-
* - Do NOT bump for additive changes that don't affect config.yml (new directories, etc.)
|
|
776
|
-
*
|
|
777
|
-
* HOW TO ADD A NEW FORMAT VERSION:
|
|
778
|
-
* 1. Add entry to FORMAT_HISTORY with detailed description
|
|
779
|
-
* 2. Implement migrate_fXX_to_fYY() function
|
|
780
|
-
* 3. Add case to migrateToLatest()
|
|
781
|
-
* 4. Update CURRENT_FORMAT
|
|
782
|
-
* 5. Add tests for the migration path
|
|
783
|
-
*
|
|
784
|
-
* FORWARD COMPATIBILITY POLICY:
|
|
785
|
-
* ConfigSchema uses Zod's strip() mode, which discards unknown fields. To prevent
|
|
786
|
-
* data loss when users mix tbd versions:
|
|
787
|
-
*
|
|
788
|
-
* 1. When changing config schema, bump the format version (e.g., f03 → f04)
|
|
789
|
-
* 2. config.ts checks format compatibility via isCompatibleFormat()
|
|
790
|
-
* 3. Older tbd versions will error with "format 'fXX' is from a newer tbd version"
|
|
791
|
-
* 4. The error tells users to upgrade: npm install -g get-tbd@latest
|
|
792
|
-
*
|
|
793
|
-
* This ensures older versions fail fast rather than silently corrupting config.
|
|
794
|
-
* See ConfigSchema in schemas.ts and checkFormatCompatibility() in config.ts.
|
|
795
|
-
*/
|
|
796
|
-
/**
|
|
797
|
-
* Current format version.
|
|
798
|
-
* Bump this ONLY for breaking changes that require migration.
|
|
799
|
-
*/
|
|
800
|
-
const CURRENT_FORMAT = "f03";
|
|
801
|
-
/**
|
|
802
|
-
* Initial format version for configs that don't have tbd_format field.
|
|
803
|
-
*/
|
|
804
|
-
const INITIAL_FORMAT = "f01";
|
|
805
|
-
/**
|
|
806
|
-
* Complete history of format versions with their changes.
|
|
807
|
-
* This serves as documentation and enables version detection.
|
|
808
|
-
*/
|
|
809
|
-
const FORMAT_HISTORY = {
|
|
810
|
-
f01: {
|
|
811
|
-
introduced: "0.1.0",
|
|
812
|
-
description: "Initial format",
|
|
813
|
-
structure: {
|
|
814
|
-
"config.yml": "Project configuration",
|
|
815
|
-
"state.yml": "Local state (gitignored)",
|
|
816
|
-
"docs/": "Documentation cache (gitignored)",
|
|
817
|
-
"issues/": "Issue YAML files"
|
|
818
|
-
}
|
|
819
|
-
},
|
|
820
|
-
f02: {
|
|
821
|
-
introduced: "0.1.5",
|
|
822
|
-
description: "Adds configurable doc_cache",
|
|
823
|
-
changes: [
|
|
824
|
-
"Added doc_cache: key to config.yml for configurable doc sources",
|
|
825
|
-
"Added settings.doc_auto_sync_hours for automatic doc refresh",
|
|
826
|
-
"Added last_doc_sync_at to state.yml for tracking sync time"
|
|
827
|
-
],
|
|
828
|
-
migration: "Populates default doc_cache config from bundled docs"
|
|
829
|
-
},
|
|
830
|
-
f03: {
|
|
831
|
-
introduced: "0.1.6",
|
|
832
|
-
description: "Consolidates docs_cache config structure",
|
|
833
|
-
changes: [
|
|
834
|
-
"Consolidated doc_cache: and docs: into single docs_cache: key",
|
|
835
|
-
"Moved doc_cache: -> docs_cache.files:",
|
|
836
|
-
"Moved docs.paths: -> docs_cache.lookup_path:",
|
|
837
|
-
"Removed separate docs: key"
|
|
838
|
-
],
|
|
839
|
-
migration: "Migrates old config keys to new docs_cache structure"
|
|
840
|
-
}
|
|
841
|
-
};
|
|
842
|
-
/**
|
|
843
|
-
* Migrate from f01 to f02.
|
|
844
|
-
* - Adds tbd_format field
|
|
845
|
-
* - Adds doc_auto_sync_hours setting (default: 24)
|
|
846
|
-
* - doc_cache will be populated separately during setup (requires file system access)
|
|
847
|
-
*/
|
|
848
|
-
function migrate_f01_to_f02(config) {
|
|
849
|
-
const changes = [];
|
|
850
|
-
const migrated = { ...config };
|
|
851
|
-
migrated.tbd_format = "f02";
|
|
852
|
-
changes.push("Added tbd_format: f02");
|
|
853
|
-
migrated.settings ??= {};
|
|
854
|
-
if (migrated.settings.doc_auto_sync_hours === void 0) {
|
|
855
|
-
migrated.settings.doc_auto_sync_hours = 24;
|
|
856
|
-
changes.push("Added settings.doc_auto_sync_hours: 24");
|
|
857
|
-
}
|
|
858
|
-
return {
|
|
859
|
-
config: migrated,
|
|
860
|
-
fromFormat: "f01",
|
|
861
|
-
toFormat: "f02",
|
|
862
|
-
changed: changes.length > 0,
|
|
863
|
-
changes
|
|
864
|
-
};
|
|
865
|
-
}
|
|
866
|
-
/**
|
|
867
|
-
* Migrate from f02 to f03.
|
|
868
|
-
* - Consolidates doc_cache: and docs: into docs_cache:
|
|
869
|
-
* - Moves doc_cache: -> docs_cache.files:
|
|
870
|
-
* - Moves docs.paths: -> docs_cache.lookup_path:
|
|
871
|
-
* - Removes separate docs: and doc_cache: keys
|
|
872
|
-
*/
|
|
873
|
-
function migrate_f02_to_f03(config) {
|
|
874
|
-
const changes = [];
|
|
875
|
-
const migrated = { ...config };
|
|
876
|
-
migrated.tbd_format = "f03";
|
|
877
|
-
changes.push("Updated tbd_format: f03");
|
|
878
|
-
migrated.docs_cache ??= {};
|
|
879
|
-
if (migrated.doc_cache && Object.keys(migrated.doc_cache).length > 0) {
|
|
880
|
-
migrated.docs_cache.files = { ...migrated.doc_cache };
|
|
881
|
-
changes.push("Moved doc_cache: -> docs_cache.files:");
|
|
882
|
-
delete migrated.doc_cache;
|
|
883
|
-
}
|
|
884
|
-
if (migrated.docs?.paths && migrated.docs.paths.length > 0) {
|
|
885
|
-
migrated.docs_cache.lookup_path = [...migrated.docs.paths];
|
|
886
|
-
changes.push("Moved docs.paths: -> docs_cache.lookup_path:");
|
|
887
|
-
}
|
|
888
|
-
if (migrated.docs) {
|
|
889
|
-
delete migrated.docs;
|
|
890
|
-
changes.push("Removed docs: key");
|
|
891
|
-
}
|
|
892
|
-
return {
|
|
893
|
-
config: migrated,
|
|
894
|
-
fromFormat: "f02",
|
|
895
|
-
toFormat: "f03",
|
|
896
|
-
changed: changes.length > 0,
|
|
897
|
-
changes
|
|
898
|
-
};
|
|
899
|
-
}
|
|
900
|
-
/**
|
|
901
|
-
* Detect the format version of a config.
|
|
902
|
-
* Returns INITIAL_FORMAT ('f01') if no tbd_format field is present.
|
|
903
|
-
*/
|
|
904
|
-
function detectFormat(config) {
|
|
905
|
-
const format = config.tbd_format;
|
|
906
|
-
if (!format) return INITIAL_FORMAT;
|
|
907
|
-
if (format in FORMAT_HISTORY) return format;
|
|
908
|
-
return CURRENT_FORMAT;
|
|
909
|
-
}
|
|
910
|
-
/**
|
|
911
|
-
* Check if a config needs migration.
|
|
912
|
-
*/
|
|
913
|
-
function needsMigration(config) {
|
|
914
|
-
return detectFormat(config) !== CURRENT_FORMAT;
|
|
915
|
-
}
|
|
916
|
-
/**
|
|
917
|
-
* Migrate a config to the latest format version.
|
|
918
|
-
*
|
|
919
|
-
* This function applies all necessary migrations in sequence.
|
|
920
|
-
* It does NOT populate doc_cache - that requires file system access
|
|
921
|
-
* and should be done separately during setup.
|
|
922
|
-
*
|
|
923
|
-
* @param config - The raw config to migrate
|
|
924
|
-
* @returns Migration result with the migrated config and change log
|
|
925
|
-
*/
|
|
926
|
-
function migrateToLatest(config) {
|
|
927
|
-
const fromFormat = detectFormat(config);
|
|
928
|
-
if (fromFormat === CURRENT_FORMAT) return {
|
|
929
|
-
config,
|
|
930
|
-
fromFormat,
|
|
931
|
-
toFormat: CURRENT_FORMAT,
|
|
932
|
-
changed: false,
|
|
933
|
-
changes: []
|
|
934
|
-
};
|
|
935
|
-
let current = config;
|
|
936
|
-
let currentFormat = fromFormat;
|
|
937
|
-
const allChanges = [];
|
|
938
|
-
if (currentFormat === "f01") {
|
|
939
|
-
const result = migrate_f01_to_f02(current);
|
|
940
|
-
current = result.config;
|
|
941
|
-
currentFormat = "f02";
|
|
942
|
-
allChanges.push(...result.changes);
|
|
943
|
-
}
|
|
944
|
-
if (currentFormat === "f02") {
|
|
945
|
-
const result = migrate_f02_to_f03(current);
|
|
946
|
-
current = result.config;
|
|
947
|
-
currentFormat = "f03";
|
|
948
|
-
allChanges.push(...result.changes);
|
|
949
|
-
}
|
|
950
|
-
return {
|
|
951
|
-
config: current,
|
|
952
|
-
fromFormat,
|
|
953
|
-
toFormat: currentFormat,
|
|
954
|
-
changed: allChanges.length > 0,
|
|
955
|
-
changes: allChanges
|
|
956
|
-
};
|
|
957
|
-
}
|
|
958
|
-
/**
|
|
959
|
-
* Check if a format version is compatible with the current tbd version.
|
|
960
|
-
* Future format versions are considered incompatible (would need tbd upgrade).
|
|
961
|
-
*/
|
|
962
|
-
function isCompatibleFormat(format) {
|
|
963
|
-
const formatVersions = Object.keys(FORMAT_HISTORY);
|
|
964
|
-
const currentIndex = formatVersions.indexOf(CURRENT_FORMAT);
|
|
965
|
-
const checkIndex = formatVersions.indexOf(format);
|
|
966
|
-
if (checkIndex === -1) return false;
|
|
967
|
-
return checkIndex <= currentIndex;
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
//#endregion
|
|
971
|
-
//#region src/file/config.ts
|
|
972
|
-
/**
|
|
973
|
-
* Config file operations.
|
|
974
|
-
*
|
|
975
|
-
* Config is stored at .tbd/config.yml and contains project-level settings.
|
|
976
|
-
*
|
|
977
|
-
* ⚠️ FORMAT VERSIONING: See tbd-format.ts for version history and migration rules.
|
|
978
|
-
*
|
|
979
|
-
* See: tbd-design.md §2.2.2 Config File
|
|
980
|
-
*/
|
|
981
|
-
/**
|
|
982
|
-
* Error thrown when the config format version is from a newer tbd version.
|
|
983
|
-
* This prevents older tbd versions from silently stripping new config fields.
|
|
984
|
-
*/
|
|
985
|
-
var IncompatibleFormatError = class extends Error {
|
|
986
|
-
constructor(foundFormat, supportedFormat) {
|
|
987
|
-
super(`Config format '${foundFormat}' is from a newer tbd version.\nThis tbd version supports up to format '${supportedFormat}'.\nPlease upgrade tbd: npm install -g get-tbd@latest`);
|
|
988
|
-
this.foundFormat = foundFormat;
|
|
989
|
-
this.supportedFormat = supportedFormat;
|
|
990
|
-
this.name = "IncompatibleFormatError";
|
|
991
|
-
}
|
|
992
|
-
};
|
|
993
|
-
/**
|
|
994
|
-
* Check if config format is compatible, throw if not.
|
|
995
|
-
* This prevents older tbd versions from silently stripping fields added by newer versions.
|
|
996
|
-
*/
|
|
997
|
-
function checkFormatCompatibility(data) {
|
|
998
|
-
const format = data.tbd_format;
|
|
999
|
-
if (format && !isCompatibleFormat(format)) throw new IncompatibleFormatError(format, CURRENT_FORMAT);
|
|
1000
|
-
}
|
|
1001
|
-
/**
|
|
1002
|
-
* Create default config for a new project.
|
|
1003
|
-
* @param prefix - Required: the project prefix for display IDs (e.g., "proj", "myapp")
|
|
1004
|
-
*/
|
|
1005
|
-
function createDefaultConfig(version, prefix) {
|
|
1006
|
-
return ConfigSchema.parse({
|
|
1007
|
-
tbd_format: CURRENT_FORMAT,
|
|
1008
|
-
tbd_version: version,
|
|
1009
|
-
sync: {
|
|
1010
|
-
branch: SYNC_BRANCH,
|
|
1011
|
-
remote: "origin"
|
|
1012
|
-
},
|
|
1013
|
-
display: { id_prefix: prefix },
|
|
1014
|
-
settings: {
|
|
1015
|
-
auto_sync: false,
|
|
1016
|
-
doc_auto_sync_hours: 24
|
|
1017
|
-
}
|
|
1018
|
-
});
|
|
1019
|
-
}
|
|
1020
|
-
/**
|
|
1021
|
-
* Initialize a new config file with default settings.
|
|
1022
|
-
* Creates .tbd directory if it doesn't exist.
|
|
1023
|
-
* @param prefix - Required: the project prefix for display IDs (e.g., "proj", "myapp")
|
|
1024
|
-
*/
|
|
1025
|
-
async function initConfig(baseDir, version, prefix) {
|
|
1026
|
-
await mkdir(join(baseDir, ".tbd"), { recursive: true });
|
|
1027
|
-
const config = createDefaultConfig(version, prefix);
|
|
1028
|
-
await writeConfig(baseDir, config);
|
|
1029
|
-
return config;
|
|
1030
|
-
}
|
|
1031
|
-
/**
|
|
1032
|
-
* Read config from file with automatic migration if needed.
|
|
1033
|
-
*
|
|
1034
|
-
* ⚠️ FORMAT VERSIONING: See tbd-format.ts for version history and migration rules.
|
|
1035
|
-
*
|
|
1036
|
-
* @throws {IncompatibleFormatError} If config is from a newer tbd version.
|
|
1037
|
-
* @throws If config file doesn't exist or is invalid.
|
|
1038
|
-
*/
|
|
1039
|
-
async function readConfig(baseDir) {
|
|
1040
|
-
const data = parse(await readFile(join(baseDir, CONFIG_FILE), "utf-8"));
|
|
1041
|
-
checkFormatCompatibility(data);
|
|
1042
|
-
if (needsMigration(data)) {
|
|
1043
|
-
const result = migrateToLatest(data);
|
|
1044
|
-
return ConfigSchema.parse(result.config);
|
|
1045
|
-
}
|
|
1046
|
-
return ConfigSchema.parse(data);
|
|
1047
|
-
}
|
|
1048
|
-
/**
|
|
1049
|
-
* Read config from file, returning migration info if a migration was applied.
|
|
1050
|
-
* Use this when you need to know if the config was migrated.
|
|
1051
|
-
*
|
|
1052
|
-
* @throws {IncompatibleFormatError} If config is from a newer tbd version.
|
|
1053
|
-
*/
|
|
1054
|
-
async function readConfigWithMigration(baseDir) {
|
|
1055
|
-
const data = parse(await readFile(join(baseDir, CONFIG_FILE), "utf-8"));
|
|
1056
|
-
checkFormatCompatibility(data);
|
|
1057
|
-
if (needsMigration(data)) {
|
|
1058
|
-
const result = migrateToLatest(data);
|
|
1059
|
-
return {
|
|
1060
|
-
config: ConfigSchema.parse(result.config),
|
|
1061
|
-
migrated: result.changed,
|
|
1062
|
-
changes: result.changes
|
|
1063
|
-
};
|
|
1064
|
-
}
|
|
1065
|
-
return {
|
|
1066
|
-
config: ConfigSchema.parse(data),
|
|
1067
|
-
migrated: false,
|
|
1068
|
-
changes: []
|
|
1069
|
-
};
|
|
1070
|
-
}
|
|
1071
|
-
/**
|
|
1072
|
-
* Write config to file with explanatory comments.
|
|
1073
|
-
*/
|
|
1074
|
-
async function writeConfig(baseDir, config) {
|
|
1075
|
-
const configPath = join(baseDir, CONFIG_FILE);
|
|
1076
|
-
let content = stringifyYaml(sortKeys(config, CONFIG_FIELD_ORDER), {
|
|
1077
|
-
lineWidth: 0,
|
|
1078
|
-
sortMapEntries: false
|
|
1079
|
-
});
|
|
1080
|
-
if (config.docs_cache && Object.keys(config.docs_cache).length > 0) content = content.replace("docs_cache:", "# Documentation cache configuration.\n# files: Maps destination paths (relative to .tbd/docs/) to source locations.\n# Sources can be:\n# - internal: prefix for bundled docs (e.g., \"internal:shortcuts/standard/code-review-and-commit.md\")\n# - Full URL for external docs (e.g., \"https://raw.githubusercontent.com/org/repo/main/file.md\")\n# lookup_path: Search paths for doc lookup (like shell $PATH). Earlier paths take precedence.\n#\n# To sync docs: tbd sync --docs\n# To check status: tbd sync --status\n#\n# Auto-sync: Docs are automatically synced when stale (default: every 24 hours).\n# Configure with settings.doc_auto_sync_hours (0 = disabled).\ndocs_cache:");
|
|
1081
|
-
await writeFile(configPath, content);
|
|
1082
|
-
}
|
|
1083
|
-
/**
|
|
1084
|
-
* Check if tbd is properly initialized in the given directory.
|
|
1085
|
-
* Returns true only if .tbd/config.yml exists (not just a .tbd/ directory).
|
|
1086
|
-
*
|
|
1087
|
-
* This prevents spurious .tbd/ directories (e.g., containing only state.yml
|
|
1088
|
-
* created by a bug) from being mistaken for tbd roots. A valid tbd root
|
|
1089
|
-
* always has config.yml created during `tbd init`.
|
|
1090
|
-
*/
|
|
1091
|
-
async function hasTbdDir(dir) {
|
|
1092
|
-
const configPath = join(dir, CONFIG_FILE);
|
|
1093
|
-
try {
|
|
1094
|
-
await access(configPath);
|
|
1095
|
-
return true;
|
|
1096
|
-
} catch {
|
|
1097
|
-
return false;
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
/**
|
|
1101
|
-
* Find the tbd repository root by walking up the directory tree.
|
|
1102
|
-
* Similar to how git finds .git/ directories.
|
|
1103
|
-
*
|
|
1104
|
-
* @param startDir - Directory to start searching from
|
|
1105
|
-
* @returns The tbd root directory path, or null if not found
|
|
1106
|
-
*/
|
|
1107
|
-
async function findTbdRoot(startDir) {
|
|
1108
|
-
let currentDir = startDir;
|
|
1109
|
-
const { root } = parse$1(startDir);
|
|
1110
|
-
while (currentDir !== root) {
|
|
1111
|
-
if (await hasTbdDir(currentDir)) return currentDir;
|
|
1112
|
-
currentDir = dirname(currentDir);
|
|
1113
|
-
}
|
|
1114
|
-
if (await hasTbdDir(root)) return root;
|
|
1115
|
-
return null;
|
|
1116
|
-
}
|
|
1117
|
-
/**
|
|
1118
|
-
* Check if tbd is initialized in the given directory or any parent directory.
|
|
1119
|
-
* Walks up the directory tree looking for .tbd/.
|
|
1120
|
-
*/
|
|
1121
|
-
async function isInitialized(baseDir) {
|
|
1122
|
-
return await findTbdRoot(baseDir) !== null;
|
|
1123
|
-
}
|
|
1124
|
-
/**
|
|
1125
|
-
* Read local state from .tbd/state.yml
|
|
1126
|
-
* Returns empty state if file doesn't exist.
|
|
1127
|
-
*/
|
|
1128
|
-
async function readLocalState(baseDir) {
|
|
1129
|
-
const statePath = join(baseDir, STATE_FILE);
|
|
1130
|
-
try {
|
|
1131
|
-
const data = parse(await readFile(statePath, "utf-8"));
|
|
1132
|
-
return LocalStateSchema.parse(data ?? {});
|
|
1133
|
-
} catch {
|
|
1134
|
-
return {};
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
/**
|
|
1138
|
-
* Write local state to .tbd/state.yml
|
|
1139
|
-
*
|
|
1140
|
-
* Uses `atomically` for safe writes (atomic rename, auto parent-dir creation).
|
|
1141
|
-
* However, we intentionally guard against .tbd/ not existing: `atomically`
|
|
1142
|
-
* would auto-create it, which is wrong if baseDir is a subdirectory rather
|
|
1143
|
-
* than the true tbd root. Only `tbd init` (via initConfig) should create .tbd/.
|
|
1144
|
-
*/
|
|
1145
|
-
async function writeLocalState(baseDir, state) {
|
|
1146
|
-
const tbdDir = join(baseDir, ".tbd");
|
|
1147
|
-
try {
|
|
1148
|
-
await access(tbdDir);
|
|
1149
|
-
} catch {
|
|
1150
|
-
throw new Error(`Cannot write state: .tbd/ directory does not exist at ${baseDir}. Run 'tbd init' first or ensure the correct tbd root is being used.`);
|
|
1151
|
-
}
|
|
1152
|
-
await writeFile(join(baseDir, STATE_FILE), stringifyYaml(sortKeys(state, LOCAL_STATE_FIELD_ORDER), {
|
|
1153
|
-
lineWidth: 0,
|
|
1154
|
-
sortMapEntries: false
|
|
1155
|
-
}));
|
|
1156
|
-
}
|
|
1157
|
-
/**
|
|
1158
|
-
* Update specific fields in local state (merge with existing).
|
|
1159
|
-
*/
|
|
1160
|
-
async function updateLocalState(baseDir, updates) {
|
|
1161
|
-
const updated = {
|
|
1162
|
-
...await readLocalState(baseDir),
|
|
1163
|
-
...updates
|
|
1164
|
-
};
|
|
1165
|
-
await writeLocalState(baseDir, updated);
|
|
1166
|
-
return updated;
|
|
1167
|
-
}
|
|
1168
|
-
/**
|
|
1169
|
-
* Check if the user has seen the welcome message.
|
|
1170
|
-
*/
|
|
1171
|
-
async function hasSeenWelcome(baseDir) {
|
|
1172
|
-
return (await readLocalState(baseDir)).welcome_seen === true;
|
|
1173
|
-
}
|
|
1174
|
-
/**
|
|
1175
|
-
* Mark the welcome message as seen.
|
|
1176
|
-
*/
|
|
1177
|
-
async function markWelcomeSeen(baseDir) {
|
|
1178
|
-
await updateLocalState(baseDir, { welcome_seen: true });
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
551
|
//#endregion
|
|
1182
552
|
//#region src/cli/lib/errors.ts
|
|
1183
553
|
/**
|
|
@@ -5312,6 +4682,9 @@ async function importFromWorkspace(tbdRoot, dataSyncDir, options) {
|
|
|
5312
4682
|
const sourceMapping = await loadIdMapping(sourceDir);
|
|
5313
4683
|
const targetMapping = await loadIdMapping(dataSyncDir);
|
|
5314
4684
|
for (const [shortId, ulid] of sourceMapping.shortToUlid) if (!targetMapping.shortToUlid.has(shortId)) addIdMapping(targetMapping, ulid, shortId);
|
|
4685
|
+
const reconcileResult = reconcileMappings(sourceIssues.map((i) => i.id), targetMapping, sourceMapping);
|
|
4686
|
+
if (reconcileResult.recovered.length > 0) log.info(`Recovered ${reconcileResult.recovered.length} ID mapping(s) from workspace`);
|
|
4687
|
+
if (reconcileResult.created.length > 0) log.info(`Created ${reconcileResult.created.length} new ID mapping(s) for imported issues`);
|
|
5315
4688
|
await saveIdMapping(dataSyncDir, targetMapping);
|
|
5316
4689
|
let cleared = false;
|
|
5317
4690
|
if (shouldClear && imported > 0) {
|
|
@@ -5466,8 +4839,8 @@ var SyncHandler = class extends BaseCommand {
|
|
|
5466
4839
|
else if (options.push) await this.pushChanges(syncBranch, remote);
|
|
5467
4840
|
else await this.fullSync(syncBranch, remote, {
|
|
5468
4841
|
force: options.force,
|
|
5469
|
-
|
|
5470
|
-
|
|
4842
|
+
autoSave: options.autoSave,
|
|
4843
|
+
outbox: options.outbox
|
|
5471
4844
|
});
|
|
5472
4845
|
}
|
|
5473
4846
|
/**
|
|
@@ -5770,6 +5143,24 @@ var SyncHandler = class extends BaseCommand {
|
|
|
5770
5143
|
await git("-C", worktreePath, "merge", `${remote}/${syncBranch}`, "-m", "tbd sync: merge remote changes");
|
|
5771
5144
|
this.output.debug(`Merged ${behindCommits} commit(s) from remote`);
|
|
5772
5145
|
if (headBeforeMerge) await this.showGitLogDebug("Commits received", `${headBeforeMerge}..${syncBranch}`);
|
|
5146
|
+
const postMergeIssues = await listIssues(this.dataSyncDir);
|
|
5147
|
+
const postMergeMapping = await loadIdMapping(this.dataSyncDir);
|
|
5148
|
+
let historicalMapping;
|
|
5149
|
+
try {
|
|
5150
|
+
const remoteIdsContent = await git("show", `${remote}/${syncBranch}:${DATA_SYNC_DIR}/mappings/ids.yml`);
|
|
5151
|
+
if (remoteIdsContent) historicalMapping = parseIdMappingFromYaml(remoteIdsContent);
|
|
5152
|
+
} catch {}
|
|
5153
|
+
const reconcileResult = reconcileMappings(postMergeIssues.map((i) => i.id), postMergeMapping, historicalMapping);
|
|
5154
|
+
const totalReconciled = reconcileResult.created.length + reconcileResult.recovered.length;
|
|
5155
|
+
if (totalReconciled > 0) {
|
|
5156
|
+
await saveIdMapping(this.dataSyncDir, postMergeMapping);
|
|
5157
|
+
await git("-C", worktreePath, "add", "-A");
|
|
5158
|
+
try {
|
|
5159
|
+
await git("-C", worktreePath, "commit", "--no-verify", "-m", `tbd sync: reconcile ${totalReconciled} missing ID mapping(s)`);
|
|
5160
|
+
} catch {}
|
|
5161
|
+
if (reconcileResult.recovered.length > 0) this.output.debug(`Recovered ${reconcileResult.recovered.length} ID mapping(s) from history`);
|
|
5162
|
+
if (reconcileResult.created.length > 0) this.output.debug(`Created ${reconcileResult.created.length} new ID mapping(s) (no history available)`);
|
|
5163
|
+
}
|
|
5773
5164
|
} catch {
|
|
5774
5165
|
this.output.info(`Merge conflict, attempting file-level resolution`);
|
|
5775
5166
|
const localIssues = await listIssues(this.dataSyncDir);
|
|
@@ -5782,18 +5173,29 @@ var SyncHandler = class extends BaseCommand {
|
|
|
5782
5173
|
} catch {
|
|
5783
5174
|
this.output.debug(`Issue ${localIssue.id} not on remote, keeping local`);
|
|
5784
5175
|
}
|
|
5176
|
+
let conflictRemoteMapping;
|
|
5785
5177
|
try {
|
|
5786
5178
|
const remoteIdsContent = await git("show", `${remote}/${syncBranch}:${DATA_SYNC_DIR}/mappings/ids.yml`);
|
|
5787
5179
|
if (remoteIdsContent) {
|
|
5180
|
+
conflictRemoteMapping = parseIdMappingFromYaml(remoteIdsContent);
|
|
5788
5181
|
const localMapping = await loadIdMapping(this.dataSyncDir);
|
|
5789
|
-
const
|
|
5790
|
-
const mergedMapping = mergeIdMappings(localMapping, remoteMapping);
|
|
5182
|
+
const mergedMapping = mergeIdMappings(localMapping, conflictRemoteMapping);
|
|
5791
5183
|
await saveIdMapping(this.dataSyncDir, mergedMapping);
|
|
5792
|
-
this.output.debug(`Merged ID mappings: ${localMapping.shortToUlid.size} local + ${
|
|
5184
|
+
this.output.debug(`Merged ID mappings: ${localMapping.shortToUlid.size} local + ${conflictRemoteMapping.shortToUlid.size} remote = ${mergedMapping.shortToUlid.size} total`);
|
|
5793
5185
|
}
|
|
5794
5186
|
} catch (error) {
|
|
5795
5187
|
this.output.debug(`Could not merge ids.yml: ${error.message}`);
|
|
5796
5188
|
}
|
|
5189
|
+
{
|
|
5190
|
+
const allIssues = await listIssues(this.dataSyncDir);
|
|
5191
|
+
const currentMapping = await loadIdMapping(this.dataSyncDir);
|
|
5192
|
+
const reconcileResult = reconcileMappings(allIssues.map((i) => i.id), currentMapping, conflictRemoteMapping);
|
|
5193
|
+
if (reconcileResult.created.length + reconcileResult.recovered.length > 0) {
|
|
5194
|
+
await saveIdMapping(this.dataSyncDir, currentMapping);
|
|
5195
|
+
if (reconcileResult.recovered.length > 0) this.output.debug(`Recovered ${reconcileResult.recovered.length} ID mapping(s) from remote`);
|
|
5196
|
+
if (reconcileResult.created.length > 0) this.output.debug(`Created ${reconcileResult.created.length} new ID mapping(s) after conflict resolution`);
|
|
5197
|
+
}
|
|
5198
|
+
}
|
|
5797
5199
|
await git("-C", worktreePath, "add", "-A");
|
|
5798
5200
|
const conflictCheck = await git("-C", worktreePath, "diff", "--cached", "-S<<<<<<< ", "--name-only");
|
|
5799
5201
|
if (conflictCheck.trim()) {
|
|
@@ -5857,7 +5259,7 @@ var SyncHandler = class extends BaseCommand {
|
|
|
5857
5259
|
this.output.error(`Push failed: ${displayError}`);
|
|
5858
5260
|
console.log(` ${aheadCommits} commit(s) not pushed to remote.`);
|
|
5859
5261
|
});
|
|
5860
|
-
if (errorType === "permanent" &&
|
|
5262
|
+
if (errorType === "permanent" && options.autoSave !== false) await this.handlePermanentFailure();
|
|
5861
5263
|
else if (!this.ctx.json) if (errorType === "transient") {
|
|
5862
5264
|
console.log("");
|
|
5863
5265
|
console.log(" This appears to be a temporary issue. Options:");
|
|
@@ -5872,7 +5274,7 @@ var SyncHandler = class extends BaseCommand {
|
|
|
5872
5274
|
}
|
|
5873
5275
|
return;
|
|
5874
5276
|
}
|
|
5875
|
-
if (
|
|
5277
|
+
if (options.outbox !== false) await this.maybeImportOutbox(syncBranch, remote);
|
|
5876
5278
|
this.output.data({
|
|
5877
5279
|
summary,
|
|
5878
5280
|
conflicts: conflicts.length
|
|
@@ -6895,6 +6297,7 @@ var DoctorHandler = class extends BaseCommand {
|
|
|
6895
6297
|
healthChecks.push(await this.checkIdMappingDuplicates(options.fix));
|
|
6896
6298
|
healthChecks.push(await this.checkTempFiles(options.fix));
|
|
6897
6299
|
healthChecks.push(this.checkIssueValidity(this.issues));
|
|
6300
|
+
healthChecks.push(await this.checkMissingMappings(options.fix));
|
|
6898
6301
|
healthChecks.push(await this.checkWorktree(options.fix));
|
|
6899
6302
|
healthChecks.push(await this.checkDataLocation(options.fix));
|
|
6900
6303
|
healthChecks.push(await this.checkLocalSyncBranch());
|
|
@@ -7119,7 +6522,7 @@ var DoctorHandler = class extends BaseCommand {
|
|
|
7119
6522
|
status: "ok"
|
|
7120
6523
|
};
|
|
7121
6524
|
if (fix && !this.checkDryRun("Fix duplicate ID mapping keys")) try {
|
|
7122
|
-
const { loadIdMapping, saveIdMapping } = await import("./id-mapping-
|
|
6525
|
+
const { loadIdMapping, saveIdMapping } = await import("./id-mapping-0-R0X8zb.mjs");
|
|
7123
6526
|
const mapping = await loadIdMapping(this.dataSyncDir);
|
|
7124
6527
|
await saveIdMapping(this.dataSyncDir, mapping);
|
|
7125
6528
|
return {
|
|
@@ -7238,6 +6641,63 @@ var DoctorHandler = class extends BaseCommand {
|
|
|
7238
6641
|
suggestion: "Manually fix or delete invalid issue files"
|
|
7239
6642
|
};
|
|
7240
6643
|
}
|
|
6644
|
+
/**
|
|
6645
|
+
* Check for issues that have no short ID mapping in ids.yml.
|
|
6646
|
+
*
|
|
6647
|
+
* This can happen when a git merge brings in issue files (e.g., from
|
|
6648
|
+
* a feature branch with outbox issues) without the corresponding
|
|
6649
|
+
* ids.yml entries. Without a mapping, any command that tries to
|
|
6650
|
+
* display the issue ID will crash.
|
|
6651
|
+
*
|
|
6652
|
+
* With --fix, creates missing mappings automatically.
|
|
6653
|
+
*/
|
|
6654
|
+
async checkMissingMappings(fix) {
|
|
6655
|
+
if (this.issues.length === 0) return {
|
|
6656
|
+
name: "ID mapping coverage",
|
|
6657
|
+
status: "ok"
|
|
6658
|
+
};
|
|
6659
|
+
const { loadIdMapping, saveIdMapping, reconcileMappings } = await import("./id-mapping-0-R0X8zb.mjs");
|
|
6660
|
+
const mapping = await loadIdMapping(this.dataSyncDir);
|
|
6661
|
+
const missingIds = [];
|
|
6662
|
+
for (const issue of this.issues) {
|
|
6663
|
+
const ulid = extractUlidFromInternalId(issue.id);
|
|
6664
|
+
if (!mapping.ulidToShort.has(ulid)) missingIds.push(issue.id);
|
|
6665
|
+
}
|
|
6666
|
+
if (missingIds.length === 0) return {
|
|
6667
|
+
name: "ID mapping coverage",
|
|
6668
|
+
status: "ok"
|
|
6669
|
+
};
|
|
6670
|
+
if (fix && !this.checkDryRun("Create missing ID mappings")) {
|
|
6671
|
+
const { parseIdMappingFromYaml } = await import("./id-mapping-0-R0X8zb.mjs");
|
|
6672
|
+
let historicalMapping;
|
|
6673
|
+
try {
|
|
6674
|
+
const syncBranch = (await import("./config-CB1tcqTZ.mjs").then((m) => m.readConfig(this.cwd))).sync.branch;
|
|
6675
|
+
const priorContent = await git("log", "-1", "--format=%H", syncBranch, "--", `${DATA_SYNC_DIR}/mappings/ids.yml`);
|
|
6676
|
+
if (priorContent.trim()) {
|
|
6677
|
+
const idsContent = await git("show", `${priorContent.trim()}:${DATA_SYNC_DIR}/mappings/ids.yml`);
|
|
6678
|
+
if (idsContent) historicalMapping = parseIdMappingFromYaml(idsContent);
|
|
6679
|
+
}
|
|
6680
|
+
} catch {}
|
|
6681
|
+
const result = reconcileMappings(missingIds, mapping, historicalMapping);
|
|
6682
|
+
await saveIdMapping(this.dataSyncDir, mapping);
|
|
6683
|
+
const parts = [];
|
|
6684
|
+
if (result.recovered.length > 0) parts.push(`recovered ${result.recovered.length} from git history`);
|
|
6685
|
+
if (result.created.length > 0) parts.push(`created ${result.created.length} new`);
|
|
6686
|
+
return {
|
|
6687
|
+
name: "ID mapping coverage",
|
|
6688
|
+
status: "ok",
|
|
6689
|
+
message: parts.join(", ")
|
|
6690
|
+
};
|
|
6691
|
+
}
|
|
6692
|
+
return {
|
|
6693
|
+
name: "ID mapping coverage",
|
|
6694
|
+
status: "error",
|
|
6695
|
+
message: `${missingIds.length} issue(s) without short ID mapping`,
|
|
6696
|
+
details: missingIds.map((id) => `${id} (no short ID)`),
|
|
6697
|
+
fixable: true,
|
|
6698
|
+
suggestion: "Run: tbd doctor --fix to create missing mappings"
|
|
6699
|
+
};
|
|
6700
|
+
}
|
|
7241
6701
|
async checkClaudeSkill() {
|
|
7242
6702
|
const claudePaths = getClaudePaths(this.cwd);
|
|
7243
6703
|
try {
|
|
@@ -10939,6 +10399,9 @@ var SetupDefaultHandler = class extends BaseCommand {
|
|
|
10939
10399
|
]);
|
|
10940
10400
|
if (tbdGitignoreResult.created) console.log(` ${colors.success("✓")} Created .tbd/.gitignore`);
|
|
10941
10401
|
else if (tbdGitignoreResult.added.length > 0) console.log(` ${colors.success("✓")} Updated .tbd/.gitignore with new patterns`);
|
|
10402
|
+
const gitattributesResult = await ensureGitignorePatterns(join(projectDir, TBD_DIR, ".gitattributes"), ["# Protect ID mappings from merge deletion (always keep all rows)", "**/mappings/ids.yml merge=union"]);
|
|
10403
|
+
if (gitattributesResult.created) console.log(` ${colors.success("✓")} Created .tbd/.gitattributes (merge protection)`);
|
|
10404
|
+
else if (gitattributesResult.added.length > 0) console.log(` ${colors.success("✓")} Updated .tbd/.gitattributes (merge protection)`);
|
|
10942
10405
|
console.log("Checking integrations...");
|
|
10943
10406
|
await new SetupAutoHandler(this.cmd).run(projectDir);
|
|
10944
10407
|
console.log("");
|
|
@@ -11069,6 +10532,9 @@ Example:
|
|
|
11069
10532
|
]);
|
|
11070
10533
|
if (tbdGitignoreResult.created) console.log(` ${colors.success("✓")} Created .tbd/.gitignore`);
|
|
11071
10534
|
else if (tbdGitignoreResult.added.length > 0) console.log(` ${colors.success("✓")} Updated .tbd/.gitignore`);
|
|
10535
|
+
const gitattributesResult = await ensureGitignorePatterns(join(cwd, TBD_DIR, ".gitattributes"), ["# Protect ID mappings from merge deletion (always keep all rows)", "**/mappings/ids.yml merge=union"]);
|
|
10536
|
+
if (gitattributesResult.created) console.log(` ${colors.success("✓")} Created .tbd/.gitattributes (merge protection)`);
|
|
10537
|
+
else if (gitattributesResult.added.length > 0) console.log(` ${colors.success("✓")} Updated .tbd/.gitattributes (merge protection)`);
|
|
11072
10538
|
try {
|
|
11073
10539
|
await initWorktree(cwd);
|
|
11074
10540
|
const health = await checkWorktreeHealth(cwd);
|
|
@@ -11441,7 +10907,7 @@ const workspaceCommand = new Command("workspace").description("Manage workspaces
|
|
|
11441
10907
|
function createProgram() {
|
|
11442
10908
|
const program = new Command().name("tbd").description("Git-native issue tracking for AI agents and humans").version(VERSION, "--version", "Show version number").helpOption("--help", "Display help for command").showHelpAfterError("(add --help for additional information)");
|
|
11443
10909
|
configureColoredHelp(program);
|
|
11444
|
-
program.option("--dry-run", "Show what would be done without making changes").option("--verbose", "Enable verbose output").option("--quiet", "Suppress non-essential output").option("--json", "Output as JSON").option("--color <when>", "Colorize output: auto, always, never", "auto").option("--
|
|
10910
|
+
program.option("--dry-run", "Show what would be done without making changes").option("--verbose", "Enable verbose output").option("--quiet", "Suppress non-essential output").option("--json", "Output as JSON").option("--color <when>", "Colorize output: auto, always, never", "auto").option("--no-sync", "Skip automatic sync after write operations").option("--debug", "Show internal IDs alongside public IDs for debugging");
|
|
11445
10911
|
program.commandsGroup("Documentation:");
|
|
11446
10912
|
program.addCommand(readmeCommand);
|
|
11447
10913
|
program.addCommand(primeCommand);
|