get-tbd 0.1.21 → 0.1.22
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/bin.mjs +184 -4
- package/dist/bin.mjs.map +1 -1
- package/dist/cli.mjs +113 -636
- 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/guidelines/tbd-sync-troubleshooting.md +27 -0
- package/dist/docs/tbd-design.md +82 -29
- 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-YXybDjVR.mjs} +3 -3
- package/dist/{src-BjMRpmMh.mjs.map → src-YXybDjVR.mjs.map} +1 -1
- package/dist/tbd +184 -4
- 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 +1 -1
- package/dist/id-mapping-BqSnxlxk.mjs +0 -3
- package/dist/yaml-utils-x_kr2IId.mjs.map +0 -1
package/dist/bin.mjs
CHANGED
|
@@ -6734,6 +6734,12 @@ const Dependency = objectType({
|
|
|
6734
6734
|
*
|
|
6735
6735
|
* Note: Fields use .nullable() in addition to .optional() because
|
|
6736
6736
|
* YAML parses `field: null` as JavaScript null, not undefined.
|
|
6737
|
+
*
|
|
6738
|
+
* Design note: We could add the short ID to this schema. We didn't originally
|
|
6739
|
+
* because it's one more field to maintain consistency around across files.
|
|
6740
|
+
* Having it here might make recovery of lost ID mappings far easier, but for
|
|
6741
|
+
* now we have more reliable management of the mappings file (ids.yml) and
|
|
6742
|
+
* consider it authoritative. See IdMappingYamlSchema (§2.6.8).
|
|
6737
6743
|
*/
|
|
6738
6744
|
const IssueSchema = BaseEntity.extend({
|
|
6739
6745
|
type: literalType("is"),
|
|
@@ -14027,7 +14033,7 @@ function serializeIssue(issue) {
|
|
|
14027
14033
|
* Package version, derived from git at build time.
|
|
14028
14034
|
* Format: X.Y.Z for releases, X.Y.Z-dev.N.hash for dev builds.
|
|
14029
14035
|
*/
|
|
14030
|
-
const VERSION$1 = "0.1.
|
|
14036
|
+
const VERSION$1 = "0.1.22";
|
|
14031
14037
|
|
|
14032
14038
|
//#endregion
|
|
14033
14039
|
//#region src/cli/lib/version.ts
|
|
@@ -97854,6 +97860,20 @@ function isCompatibleFormat(format) {
|
|
|
97854
97860
|
*
|
|
97855
97861
|
* See: tbd-design.md §2.2.2 Config File
|
|
97856
97862
|
*/
|
|
97863
|
+
var config_exports = /* @__PURE__ */ __exportAll({
|
|
97864
|
+
IncompatibleFormatError: () => IncompatibleFormatError,
|
|
97865
|
+
findTbdRoot: () => findTbdRoot,
|
|
97866
|
+
hasSeenWelcome: () => hasSeenWelcome,
|
|
97867
|
+
initConfig: () => initConfig,
|
|
97868
|
+
isInitialized: () => isInitialized,
|
|
97869
|
+
markWelcomeSeen: () => markWelcomeSeen,
|
|
97870
|
+
readConfig: () => readConfig,
|
|
97871
|
+
readConfigWithMigration: () => readConfigWithMigration,
|
|
97872
|
+
readLocalState: () => readLocalState,
|
|
97873
|
+
updateLocalState: () => updateLocalState,
|
|
97874
|
+
writeConfig: () => writeConfig,
|
|
97875
|
+
writeLocalState: () => writeLocalState
|
|
97876
|
+
});
|
|
97857
97877
|
/**
|
|
97858
97878
|
* Error thrown when the config format version is from a newer tbd version.
|
|
97859
97879
|
* This prevents older tbd versions from silently stripping new config fields.
|
|
@@ -99852,11 +99872,13 @@ function naturalSort(arr) {
|
|
|
99852
99872
|
var id_mapping_exports = /* @__PURE__ */ __exportAll({
|
|
99853
99873
|
addIdMapping: () => addIdMapping,
|
|
99854
99874
|
calculateOptimalLength: () => calculateOptimalLength,
|
|
99875
|
+
createShortIdMapping: () => createShortIdMapping,
|
|
99855
99876
|
generateUniqueShortId: () => generateUniqueShortId,
|
|
99856
99877
|
hasShortId: () => hasShortId,
|
|
99857
99878
|
loadIdMapping: () => loadIdMapping,
|
|
99858
99879
|
mergeIdMappings: () => mergeIdMappings,
|
|
99859
99880
|
parseIdMappingFromYaml: () => parseIdMappingFromYaml,
|
|
99881
|
+
reconcileMappings: () => reconcileMappings,
|
|
99860
99882
|
resolveToInternalId: () => resolveToInternalId,
|
|
99861
99883
|
saveIdMapping: () => saveIdMapping
|
|
99862
99884
|
});
|
|
@@ -99955,6 +99977,22 @@ function hasShortId(mapping, shortId) {
|
|
|
99955
99977
|
return mapping.shortToUlid.has(shortId);
|
|
99956
99978
|
}
|
|
99957
99979
|
/**
|
|
99980
|
+
* Create a short ID mapping for a new internal ID.
|
|
99981
|
+
* Generates a unique short ID and registers it in the mapping.
|
|
99982
|
+
*
|
|
99983
|
+
* @param internalId - The internal ID (is-{ulid})
|
|
99984
|
+
* @param mapping - The ID mapping to update
|
|
99985
|
+
* @returns The generated short ID
|
|
99986
|
+
*/
|
|
99987
|
+
function createShortIdMapping(internalId, mapping) {
|
|
99988
|
+
const ulid = extractUlidFromInternalId(internalId);
|
|
99989
|
+
const existing = mapping.ulidToShort.get(ulid);
|
|
99990
|
+
if (existing) return existing;
|
|
99991
|
+
const shortId = generateUniqueShortId(mapping);
|
|
99992
|
+
addIdMapping(mapping, ulid, shortId);
|
|
99993
|
+
return shortId;
|
|
99994
|
+
}
|
|
99995
|
+
/**
|
|
99958
99996
|
* Resolve any ID input to an internal ID ({prefix}-{ulid}).
|
|
99959
99997
|
*
|
|
99960
99998
|
* Handles:
|
|
@@ -100001,6 +100039,44 @@ function parseIdMappingFromYaml(content) {
|
|
|
100001
100039
|
};
|
|
100002
100040
|
}
|
|
100003
100041
|
/**
|
|
100042
|
+
* Ensure all given internal IDs have short ID mappings.
|
|
100043
|
+
* Creates missing mappings for any IDs without entries.
|
|
100044
|
+
*
|
|
100045
|
+
* This repairs state after git merges that may add issue files
|
|
100046
|
+
* without corresponding mapping entries (e.g., when outbox issues
|
|
100047
|
+
* are merged from a feature branch but ids.yml doesn't include them).
|
|
100048
|
+
*
|
|
100049
|
+
* When a `historicalMapping` is provided, the function will try to recover
|
|
100050
|
+
* the original short ID from that mapping before generating a new random one.
|
|
100051
|
+
* This preserves ID stability so that existing references (in docs, PRs,
|
|
100052
|
+
* conversations) remain valid.
|
|
100053
|
+
*
|
|
100054
|
+
* @param internalIds - Array of internal IDs (is-{ulid}) to reconcile
|
|
100055
|
+
* @param mapping - The ID mapping to update (mutated in-place)
|
|
100056
|
+
* @param historicalMapping - Optional mapping from prior state (e.g., git history) to recover original short IDs
|
|
100057
|
+
* @returns Object with `created` (IDs that got new random short IDs) and `recovered` (IDs restored from history)
|
|
100058
|
+
*/
|
|
100059
|
+
function reconcileMappings(internalIds, mapping, historicalMapping) {
|
|
100060
|
+
const created = [];
|
|
100061
|
+
const recovered = [];
|
|
100062
|
+
for (const id of internalIds) {
|
|
100063
|
+
const ulid = extractUlidFromInternalId(id);
|
|
100064
|
+
if (mapping.ulidToShort.has(ulid)) continue;
|
|
100065
|
+
const historicalShortId = historicalMapping?.ulidToShort.get(ulid);
|
|
100066
|
+
if (historicalShortId && !mapping.shortToUlid.has(historicalShortId)) {
|
|
100067
|
+
addIdMapping(mapping, ulid, historicalShortId);
|
|
100068
|
+
recovered.push(id);
|
|
100069
|
+
} else {
|
|
100070
|
+
createShortIdMapping(id, mapping);
|
|
100071
|
+
created.push(id);
|
|
100072
|
+
}
|
|
100073
|
+
}
|
|
100074
|
+
return {
|
|
100075
|
+
created,
|
|
100076
|
+
recovered
|
|
100077
|
+
};
|
|
100078
|
+
}
|
|
100079
|
+
/**
|
|
100004
100080
|
* Merge two ID mappings by combining all entries from both.
|
|
100005
100081
|
* ID mappings are always additive (new IDs are only added, never removed),
|
|
100006
100082
|
* so merging simply unions all key-value pairs.
|
|
@@ -102768,6 +102844,9 @@ async function importFromWorkspace(tbdRoot, dataSyncDir, options) {
|
|
|
102768
102844
|
const sourceMapping = await loadIdMapping(sourceDir);
|
|
102769
102845
|
const targetMapping = await loadIdMapping(dataSyncDir);
|
|
102770
102846
|
for (const [shortId, ulid] of sourceMapping.shortToUlid) if (!targetMapping.shortToUlid.has(shortId)) addIdMapping(targetMapping, ulid, shortId);
|
|
102847
|
+
const reconcileResult = reconcileMappings(sourceIssues.map((i) => i.id), targetMapping, sourceMapping);
|
|
102848
|
+
if (reconcileResult.recovered.length > 0) log.info(`Recovered ${reconcileResult.recovered.length} ID mapping(s) from workspace`);
|
|
102849
|
+
if (reconcileResult.created.length > 0) log.info(`Created ${reconcileResult.created.length} new ID mapping(s) for imported issues`);
|
|
102771
102850
|
await saveIdMapping(dataSyncDir, targetMapping);
|
|
102772
102851
|
let cleared = false;
|
|
102773
102852
|
if (shouldClear && imported > 0) {
|
|
@@ -103226,6 +103305,24 @@ var SyncHandler = class extends BaseCommand {
|
|
|
103226
103305
|
await git("-C", worktreePath, "merge", `${remote}/${syncBranch}`, "-m", "tbd sync: merge remote changes");
|
|
103227
103306
|
this.output.debug(`Merged ${behindCommits} commit(s) from remote`);
|
|
103228
103307
|
if (headBeforeMerge) await this.showGitLogDebug("Commits received", `${headBeforeMerge}..${syncBranch}`);
|
|
103308
|
+
const postMergeIssues = await listIssues(this.dataSyncDir);
|
|
103309
|
+
const postMergeMapping = await loadIdMapping(this.dataSyncDir);
|
|
103310
|
+
let historicalMapping;
|
|
103311
|
+
try {
|
|
103312
|
+
const remoteIdsContent = await git("show", `${remote}/${syncBranch}:${DATA_SYNC_DIR}/mappings/ids.yml`);
|
|
103313
|
+
if (remoteIdsContent) historicalMapping = parseIdMappingFromYaml(remoteIdsContent);
|
|
103314
|
+
} catch {}
|
|
103315
|
+
const reconcileResult = reconcileMappings(postMergeIssues.map((i) => i.id), postMergeMapping, historicalMapping);
|
|
103316
|
+
const totalReconciled = reconcileResult.created.length + reconcileResult.recovered.length;
|
|
103317
|
+
if (totalReconciled > 0) {
|
|
103318
|
+
await saveIdMapping(this.dataSyncDir, postMergeMapping);
|
|
103319
|
+
await git("-C", worktreePath, "add", "-A");
|
|
103320
|
+
try {
|
|
103321
|
+
await git("-C", worktreePath, "commit", "--no-verify", "-m", `tbd sync: reconcile ${totalReconciled} missing ID mapping(s)`);
|
|
103322
|
+
} catch {}
|
|
103323
|
+
if (reconcileResult.recovered.length > 0) this.output.debug(`Recovered ${reconcileResult.recovered.length} ID mapping(s) from history`);
|
|
103324
|
+
if (reconcileResult.created.length > 0) this.output.debug(`Created ${reconcileResult.created.length} new ID mapping(s) (no history available)`);
|
|
103325
|
+
}
|
|
103229
103326
|
} catch {
|
|
103230
103327
|
this.output.info(`Merge conflict, attempting file-level resolution`);
|
|
103231
103328
|
const localIssues = await listIssues(this.dataSyncDir);
|
|
@@ -103238,18 +103335,29 @@ var SyncHandler = class extends BaseCommand {
|
|
|
103238
103335
|
} catch {
|
|
103239
103336
|
this.output.debug(`Issue ${localIssue.id} not on remote, keeping local`);
|
|
103240
103337
|
}
|
|
103338
|
+
let conflictRemoteMapping;
|
|
103241
103339
|
try {
|
|
103242
103340
|
const remoteIdsContent = await git("show", `${remote}/${syncBranch}:${DATA_SYNC_DIR}/mappings/ids.yml`);
|
|
103243
103341
|
if (remoteIdsContent) {
|
|
103342
|
+
conflictRemoteMapping = parseIdMappingFromYaml(remoteIdsContent);
|
|
103244
103343
|
const localMapping = await loadIdMapping(this.dataSyncDir);
|
|
103245
|
-
const
|
|
103246
|
-
const mergedMapping = mergeIdMappings(localMapping, remoteMapping);
|
|
103344
|
+
const mergedMapping = mergeIdMappings(localMapping, conflictRemoteMapping);
|
|
103247
103345
|
await saveIdMapping(this.dataSyncDir, mergedMapping);
|
|
103248
|
-
this.output.debug(`Merged ID mappings: ${localMapping.shortToUlid.size} local + ${
|
|
103346
|
+
this.output.debug(`Merged ID mappings: ${localMapping.shortToUlid.size} local + ${conflictRemoteMapping.shortToUlid.size} remote = ${mergedMapping.shortToUlid.size} total`);
|
|
103249
103347
|
}
|
|
103250
103348
|
} catch (error) {
|
|
103251
103349
|
this.output.debug(`Could not merge ids.yml: ${error.message}`);
|
|
103252
103350
|
}
|
|
103351
|
+
{
|
|
103352
|
+
const allIssues = await listIssues(this.dataSyncDir);
|
|
103353
|
+
const currentMapping = await loadIdMapping(this.dataSyncDir);
|
|
103354
|
+
const reconcileResult = reconcileMappings(allIssues.map((i) => i.id), currentMapping, conflictRemoteMapping);
|
|
103355
|
+
if (reconcileResult.created.length + reconcileResult.recovered.length > 0) {
|
|
103356
|
+
await saveIdMapping(this.dataSyncDir, currentMapping);
|
|
103357
|
+
if (reconcileResult.recovered.length > 0) this.output.debug(`Recovered ${reconcileResult.recovered.length} ID mapping(s) from remote`);
|
|
103358
|
+
if (reconcileResult.created.length > 0) this.output.debug(`Created ${reconcileResult.created.length} new ID mapping(s) after conflict resolution`);
|
|
103359
|
+
}
|
|
103360
|
+
}
|
|
103253
103361
|
await git("-C", worktreePath, "add", "-A");
|
|
103254
103362
|
const conflictCheck = await git("-C", worktreePath, "diff", "--cached", "-S<<<<<<< ", "--name-only");
|
|
103255
103363
|
if (conflictCheck.trim()) {
|
|
@@ -104351,6 +104459,7 @@ var DoctorHandler = class extends BaseCommand {
|
|
|
104351
104459
|
healthChecks.push(await this.checkIdMappingDuplicates(options.fix));
|
|
104352
104460
|
healthChecks.push(await this.checkTempFiles(options.fix));
|
|
104353
104461
|
healthChecks.push(this.checkIssueValidity(this.issues));
|
|
104462
|
+
healthChecks.push(await this.checkMissingMappings(options.fix));
|
|
104354
104463
|
healthChecks.push(await this.checkWorktree(options.fix));
|
|
104355
104464
|
healthChecks.push(await this.checkDataLocation(options.fix));
|
|
104356
104465
|
healthChecks.push(await this.checkLocalSyncBranch());
|
|
@@ -104694,6 +104803,63 @@ var DoctorHandler = class extends BaseCommand {
|
|
|
104694
104803
|
suggestion: "Manually fix or delete invalid issue files"
|
|
104695
104804
|
};
|
|
104696
104805
|
}
|
|
104806
|
+
/**
|
|
104807
|
+
* Check for issues that have no short ID mapping in ids.yml.
|
|
104808
|
+
*
|
|
104809
|
+
* This can happen when a git merge brings in issue files (e.g., from
|
|
104810
|
+
* a feature branch with outbox issues) without the corresponding
|
|
104811
|
+
* ids.yml entries. Without a mapping, any command that tries to
|
|
104812
|
+
* display the issue ID will crash.
|
|
104813
|
+
*
|
|
104814
|
+
* With --fix, creates missing mappings automatically.
|
|
104815
|
+
*/
|
|
104816
|
+
async checkMissingMappings(fix) {
|
|
104817
|
+
if (this.issues.length === 0) return {
|
|
104818
|
+
name: "ID mapping coverage",
|
|
104819
|
+
status: "ok"
|
|
104820
|
+
};
|
|
104821
|
+
const { loadIdMapping, saveIdMapping, reconcileMappings } = await Promise.resolve().then(() => id_mapping_exports);
|
|
104822
|
+
const mapping = await loadIdMapping(this.dataSyncDir);
|
|
104823
|
+
const missingIds = [];
|
|
104824
|
+
for (const issue of this.issues) {
|
|
104825
|
+
const ulid = extractUlidFromInternalId(issue.id);
|
|
104826
|
+
if (!mapping.ulidToShort.has(ulid)) missingIds.push(issue.id);
|
|
104827
|
+
}
|
|
104828
|
+
if (missingIds.length === 0) return {
|
|
104829
|
+
name: "ID mapping coverage",
|
|
104830
|
+
status: "ok"
|
|
104831
|
+
};
|
|
104832
|
+
if (fix && !this.checkDryRun("Create missing ID mappings")) {
|
|
104833
|
+
const { parseIdMappingFromYaml } = await Promise.resolve().then(() => id_mapping_exports);
|
|
104834
|
+
let historicalMapping;
|
|
104835
|
+
try {
|
|
104836
|
+
const syncBranch = (await Promise.resolve().then(() => config_exports).then((m) => m.readConfig(this.cwd))).sync.branch;
|
|
104837
|
+
const priorContent = await git("log", "-1", "--format=%H", syncBranch, "--", `${DATA_SYNC_DIR}/mappings/ids.yml`);
|
|
104838
|
+
if (priorContent.trim()) {
|
|
104839
|
+
const idsContent = await git("show", `${priorContent.trim()}:${DATA_SYNC_DIR}/mappings/ids.yml`);
|
|
104840
|
+
if (idsContent) historicalMapping = parseIdMappingFromYaml(idsContent);
|
|
104841
|
+
}
|
|
104842
|
+
} catch {}
|
|
104843
|
+
const result = reconcileMappings(missingIds, mapping, historicalMapping);
|
|
104844
|
+
await saveIdMapping(this.dataSyncDir, mapping);
|
|
104845
|
+
const parts = [];
|
|
104846
|
+
if (result.recovered.length > 0) parts.push(`recovered ${result.recovered.length} from git history`);
|
|
104847
|
+
if (result.created.length > 0) parts.push(`created ${result.created.length} new`);
|
|
104848
|
+
return {
|
|
104849
|
+
name: "ID mapping coverage",
|
|
104850
|
+
status: "ok",
|
|
104851
|
+
message: parts.join(", ")
|
|
104852
|
+
};
|
|
104853
|
+
}
|
|
104854
|
+
return {
|
|
104855
|
+
name: "ID mapping coverage",
|
|
104856
|
+
status: "error",
|
|
104857
|
+
message: `${missingIds.length} issue(s) without short ID mapping`,
|
|
104858
|
+
details: missingIds.map((id) => `${id} (no short ID)`),
|
|
104859
|
+
fixable: true,
|
|
104860
|
+
suggestion: "Run: tbd doctor --fix to create missing mappings"
|
|
104861
|
+
};
|
|
104862
|
+
}
|
|
104697
104863
|
async checkClaudeSkill() {
|
|
104698
104864
|
const claudePaths = getClaudePaths(this.cwd);
|
|
104699
104865
|
try {
|
|
@@ -108468,6 +108634,13 @@ var SetupDefaultHandler = class extends BaseCommand {
|
|
|
108468
108634
|
]);
|
|
108469
108635
|
if (tbdGitignoreResult.created) console.log(` ${colors.success("✓")} Created .tbd/.gitignore`);
|
|
108470
108636
|
else if (tbdGitignoreResult.added.length > 0) console.log(` ${colors.success("✓")} Updated .tbd/.gitignore with new patterns`);
|
|
108637
|
+
const gitattributesResult = await ensureGitignorePatterns(join(projectDir, TBD_DIR, ".gitattributes"), [
|
|
108638
|
+
"# Protect ID mappings from merge deletion (always keep all rows)",
|
|
108639
|
+
"# See: https://github.com/jlevy/tbd/issues/99",
|
|
108640
|
+
"**/mappings/ids.yml merge=union"
|
|
108641
|
+
]);
|
|
108642
|
+
if (gitattributesResult.created) console.log(` ${colors.success("✓")} Created .tbd/.gitattributes (merge protection)`);
|
|
108643
|
+
else if (gitattributesResult.added.length > 0) console.log(` ${colors.success("✓")} Updated .tbd/.gitattributes (merge protection)`);
|
|
108471
108644
|
console.log("Checking integrations...");
|
|
108472
108645
|
await new SetupAutoHandler(this.cmd).run(projectDir);
|
|
108473
108646
|
console.log("");
|
|
@@ -108598,6 +108771,13 @@ Example:
|
|
|
108598
108771
|
]);
|
|
108599
108772
|
if (tbdGitignoreResult.created) console.log(` ${colors.success("✓")} Created .tbd/.gitignore`);
|
|
108600
108773
|
else if (tbdGitignoreResult.added.length > 0) console.log(` ${colors.success("✓")} Updated .tbd/.gitignore`);
|
|
108774
|
+
const gitattributesResult = await ensureGitignorePatterns(join(cwd, TBD_DIR, ".gitattributes"), [
|
|
108775
|
+
"# Protect ID mappings from merge deletion (always keep all rows)",
|
|
108776
|
+
"# See: https://github.com/jlevy/tbd/issues/99",
|
|
108777
|
+
"**/mappings/ids.yml merge=union"
|
|
108778
|
+
]);
|
|
108779
|
+
if (gitattributesResult.created) console.log(` ${colors.success("✓")} Created .tbd/.gitattributes (merge protection)`);
|
|
108780
|
+
else if (gitattributesResult.added.length > 0) console.log(` ${colors.success("✓")} Updated .tbd/.gitattributes (merge protection)`);
|
|
108601
108781
|
try {
|
|
108602
108782
|
await initWorktree(cwd);
|
|
108603
108783
|
const health = await checkWorktreeHealth(cwd);
|