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.
@@ -0,0 +1,3 @@
1
+ import { a as isInitialized, c as readConfigWithMigration, d as writeConfig, f as writeLocalState, i as initConfig, l as readLocalState, n as findTbdRoot, o as markWelcomeSeen, r as hasSeenWelcome, s as readConfig, t as IncompatibleFormatError, u as updateLocalState } from "./config-CmEAGaxz.mjs";
2
+
3
+ export { readConfig };
@@ -0,0 +1,637 @@
1
+ import { A as LOCAL_STATE_FIELD_ORDER, a as stringifyYaml, h as ConfigSchema, i as sortKeys, j as LocalStateSchema, m as CONFIG_FIELD_ORDER } from "./yaml-utils-U7l9hhkh.mjs";
2
+ import { parse } from "yaml";
3
+ import { access, mkdir, readFile } from "node:fs/promises";
4
+ import { dirname, isAbsolute, join, parse as parse$1 } from "node:path";
5
+ import { writeFile } from "atomically";
6
+ import { homedir } from "node:os";
7
+
8
+ //#region src/lib/paths.ts
9
+ /**
10
+ * Centralized path constants for tbd.
11
+ *
12
+ * Directory structure (per spec):
13
+ *
14
+ * On main/dev branches:
15
+ * .tbd/
16
+ * Committed to the repo:
17
+ * config.yml - Project configuration
18
+ * .gitignore - Controls what's gitignored below
19
+ * workspaces/ - Persistent state (outbox, named workspaces)
20
+ * Gitignored (local only):
21
+ * state.yml - Local state
22
+ * docs/ - Installed documentation (regenerated on setup)
23
+ * data-sync-worktree/ - Hidden worktree checkout of tbd-sync branch
24
+ * .tbd/data-sync/ - issues/, mappings/, attic/, meta.yml
25
+ *
26
+ * On tbd-sync branch:
27
+ * .tbd/
28
+ * data-sync/
29
+ * issues/
30
+ * mappings/
31
+ * attic/
32
+ * meta.yml
33
+ */
34
+ /** The tbd configuration directory on main branch */
35
+ const TBD_DIR = ".tbd";
36
+ /** The config file path */
37
+ const CONFIG_FILE = join(TBD_DIR, "config.yml");
38
+ /** The local state file (gitignored) */
39
+ const STATE_FILE = join(TBD_DIR, "state.yml");
40
+ /** The worktree directory name */
41
+ const WORKTREE_DIR_NAME = "data-sync-worktree";
42
+ /** The worktree path (gitignored) */
43
+ const WORKTREE_DIR = join(TBD_DIR, WORKTREE_DIR_NAME);
44
+ /** The data directory name on the sync branch */
45
+ const DATA_SYNC_DIR_NAME = "data-sync";
46
+ /**
47
+ * The base directory for synced data.
48
+ *
49
+ * NOTE: This is currently pointing directly to .tbd/data-sync/ which is WRONG
50
+ * per the spec. The correct path should be via the worktree:
51
+ * .tbd/data-sync-worktree/.tbd/data-sync/
52
+ *
53
+ * TODO(tbd-208): Update this to use the worktree path once worktree
54
+ * management is implemented.
55
+ */
56
+ const DATA_SYNC_DIR = join(TBD_DIR, DATA_SYNC_DIR_NAME);
57
+ /**
58
+ * The correct path for synced data via worktree (per spec).
59
+ * Use this once worktree management is implemented.
60
+ */
61
+ const DATA_SYNC_DIR_VIA_WORKTREE = join(WORKTREE_DIR, TBD_DIR, DATA_SYNC_DIR_NAME);
62
+ /** Issues directory */
63
+ const ISSUES_DIR = join(DATA_SYNC_DIR, "issues");
64
+ /** Mappings directory */
65
+ const MAPPINGS_DIR = join(DATA_SYNC_DIR, "mappings");
66
+ /** Attic directory for conflict resolution */
67
+ const ATTIC_DIR = join(DATA_SYNC_DIR, "attic");
68
+ /** Meta file for schema version */
69
+ const META_FILE = join(DATA_SYNC_DIR, "meta.yml");
70
+ /** The sync branch name */
71
+ const SYNC_BRANCH = "tbd-sync";
72
+ /** The workspaces directory name within .tbd/ */
73
+ const WORKSPACES_DIR_NAME = "workspaces";
74
+ /** Full path to workspaces directory: .tbd/workspaces/ */
75
+ const WORKSPACES_DIR = join(TBD_DIR, WORKSPACES_DIR_NAME);
76
+ /**
77
+ * Get the path to a named workspace directory.
78
+ *
79
+ * Workspaces are stored at: .tbd/workspaces/{name}/
80
+ *
81
+ * @param workspaceName - The name of the workspace (e.g., 'outbox', 'my-feature')
82
+ * @returns Path to the workspace directory
83
+ */
84
+ function getWorkspaceDir(workspaceName) {
85
+ return join(WORKSPACES_DIR, workspaceName);
86
+ }
87
+ /**
88
+ * Validate a workspace name.
89
+ *
90
+ * Valid workspace names:
91
+ * - Lowercase alphanumeric characters
92
+ * - Hyphens and underscores allowed
93
+ * - Must not be empty
94
+ * - Must not contain path separators or dots at start
95
+ *
96
+ * @param name - The workspace name to validate
97
+ * @returns true if the name is valid
98
+ */
99
+ function isValidWorkspaceName(name) {
100
+ if (!name || name.length === 0) return false;
101
+ if (name.startsWith(".")) return false;
102
+ return /^[a-z0-9][a-z0-9_-]*$/.test(name);
103
+ }
104
+ /** Docs directory name within .tbd/ */
105
+ const DOCS_DIR = "docs";
106
+ /** Shortcuts directory name within docs/ */
107
+ const SHORTCUTS_DIR = "shortcuts";
108
+ /** System shortcuts directory name (core docs like skill-baseline.md) */
109
+ const SYSTEM_DIR = "system";
110
+ /** Standard shortcuts directory name (workflow shortcuts) */
111
+ const STANDARD_DIR = "standard";
112
+ /** Guidelines directory name (coding rules and best practices) */
113
+ const GUIDELINES_DIR = "guidelines";
114
+ /** Templates directory name (document templates) */
115
+ const TEMPLATES_DIR = "templates";
116
+ /** Full path to docs directory: .tbd/docs/ */
117
+ const TBD_DOCS_DIR = join(TBD_DIR, DOCS_DIR);
118
+ /** Full path to shortcuts directory: .tbd/docs/shortcuts/ */
119
+ const TBD_SHORTCUTS_DIR = join(TBD_DOCS_DIR, SHORTCUTS_DIR);
120
+ /** Full path to system shortcuts: .tbd/docs/shortcuts/system/ */
121
+ const TBD_SHORTCUTS_SYSTEM = join(TBD_SHORTCUTS_DIR, SYSTEM_DIR);
122
+ /** Full path to standard shortcuts: .tbd/docs/shortcuts/standard/ */
123
+ const TBD_SHORTCUTS_STANDARD = join(TBD_SHORTCUTS_DIR, STANDARD_DIR);
124
+ /** Full path to guidelines: .tbd/docs/guidelines/ (top-level, not under shortcuts) */
125
+ const TBD_GUIDELINES_DIR = join(TBD_DOCS_DIR, GUIDELINES_DIR);
126
+ /** Full path to templates: .tbd/docs/templates/ (top-level, not under shortcuts) */
127
+ const TBD_TEMPLATES_DIR = join(TBD_DOCS_DIR, TEMPLATES_DIR);
128
+ /** Built-in docs source paths (relative to package docs/) */
129
+ const BUILTIN_SHORTCUTS_SYSTEM = join(SHORTCUTS_DIR, SYSTEM_DIR);
130
+ const BUILTIN_SHORTCUTS_STANDARD = join(SHORTCUTS_DIR, STANDARD_DIR);
131
+ /**
132
+ * Default shortcut lookup paths (searched in order, relative to tbd root).
133
+ * Earlier paths take precedence over later paths.
134
+ * Note: Guidelines and templates are now separate top-level directories.
135
+ */
136
+ const DEFAULT_SHORTCUT_PATHS = [TBD_SHORTCUTS_SYSTEM, TBD_SHORTCUTS_STANDARD];
137
+ /**
138
+ * Default guidelines lookup paths (relative to tbd root).
139
+ */
140
+ const DEFAULT_GUIDELINES_PATHS = [TBD_GUIDELINES_DIR];
141
+ /**
142
+ * Default template lookup paths (relative to tbd root).
143
+ */
144
+ const DEFAULT_TEMPLATE_PATHS = [TBD_TEMPLATES_DIR];
145
+ /**
146
+ * Error thrown when worktree is missing and fallback is not allowed.
147
+ * Defined inline to avoid circular dependency with errors.ts.
148
+ */
149
+ var WorktreeMissingError = class extends Error {
150
+ constructor(message = "Worktree not found at .tbd/data-sync-worktree/. Run 'tbd doctor --fix' to repair.") {
151
+ super(message);
152
+ this.name = "WorktreeMissingError";
153
+ }
154
+ };
155
+ /**
156
+ * Cache for resolved data sync directory.
157
+ * Reset when baseDir changes.
158
+ */
159
+ let _resolvedDataSyncDir = null;
160
+ let _resolvedBaseDir = null;
161
+ let _resolvedAllowFallback = null;
162
+ /**
163
+ * Resolve the actual data sync directory path.
164
+ *
165
+ * This function detects whether we're running with a git worktree
166
+ * (production) or in a test environment without worktree.
167
+ *
168
+ * Order of preference:
169
+ * 1. Worktree path if worktree exists: .tbd/data-sync-worktree/.tbd/data-sync/
170
+ * 2. Direct path as fallback (only if allowFallback: true)
171
+ *
172
+ * @param baseDir - The tbd root directory (from requireInit or findTbdRoot)
173
+ * @param options - Options for path resolution
174
+ * @returns Resolved data sync directory path
175
+ * @throws WorktreeMissingError if worktree missing and allowFallback is false
176
+ *
177
+ * See: plan-2026-01-28-sync-worktree-recovery-and-hardening.md
178
+ */
179
+ async function resolveDataSyncDir(baseDir, options) {
180
+ const allowFallback = options?.allowFallback ?? true;
181
+ if (_resolvedDataSyncDir && _resolvedBaseDir === baseDir && _resolvedAllowFallback === allowFallback) return _resolvedDataSyncDir;
182
+ const worktreePath = join(baseDir, DATA_SYNC_DIR_VIA_WORKTREE);
183
+ const directPath = join(baseDir, DATA_SYNC_DIR);
184
+ try {
185
+ await access(worktreePath);
186
+ _resolvedDataSyncDir = worktreePath;
187
+ _resolvedBaseDir = baseDir;
188
+ _resolvedAllowFallback = allowFallback;
189
+ return worktreePath;
190
+ } catch {
191
+ if (!allowFallback) throw new WorktreeMissingError();
192
+ if (process.env.DEBUG || process.env.TBD_DEBUG) console.warn("[tbd:paths] resolveDataSyncDir: worktree not found, falling back to direct path");
193
+ _resolvedDataSyncDir = directPath;
194
+ _resolvedBaseDir = baseDir;
195
+ _resolvedAllowFallback = allowFallback;
196
+ return directPath;
197
+ }
198
+ }
199
+ /**
200
+ * Resolve attic directory path.
201
+ */
202
+ async function resolveAtticDir(baseDir, options) {
203
+ return join(await resolveDataSyncDir(baseDir, options), "attic");
204
+ }
205
+ /**
206
+ * Characters per token ratio for estimating token counts.
207
+ *
208
+ * Based on research of OpenAI (tiktoken) and Claude tokenizers:
209
+ * - Pure English prose: ~4-5 chars/token
210
+ * - Code and symbols: ~3 chars/token
211
+ * - Mixed markdown/code docs: ~3.5 chars/token
212
+ *
213
+ * We use 3.5 as our docs are markdown with code examples.
214
+ * This provides ~15-20% accuracy, sufficient for cost estimation.
215
+ */
216
+ const CHARS_PER_TOKEN = 3.5;
217
+
218
+ //#endregion
219
+ //#region src/lib/tbd-format.ts
220
+ /**
221
+ * tbd Directory Format Versioning
222
+ * ================================
223
+ *
224
+ * This file is the SINGLE SOURCE OF TRUTH for .tbd/ directory format versions.
225
+ *
226
+ * WHEN TO BUMP THE FORMAT VERSION:
227
+ * - Bump when changes REQUIRE migration (deleting files, changing formats, moving files)
228
+ * - **Bump when changing config schema** (adding, removing, or modifying fields)
229
+ * - Do NOT bump for additive changes that don't affect config.yml (new directories, etc.)
230
+ *
231
+ * HOW TO ADD A NEW FORMAT VERSION:
232
+ * 1. Add entry to FORMAT_HISTORY with detailed description
233
+ * 2. Implement migrate_fXX_to_fYY() function
234
+ * 3. Add case to migrateToLatest()
235
+ * 4. Update CURRENT_FORMAT
236
+ * 5. Add tests for the migration path
237
+ *
238
+ * FORWARD COMPATIBILITY POLICY:
239
+ * ConfigSchema uses Zod's strip() mode, which discards unknown fields. To prevent
240
+ * data loss when users mix tbd versions:
241
+ *
242
+ * 1. When changing config schema, bump the format version (e.g., f03 → f04)
243
+ * 2. config.ts checks format compatibility via isCompatibleFormat()
244
+ * 3. Older tbd versions will error with "format 'fXX' is from a newer tbd version"
245
+ * 4. The error tells users to upgrade: npm install -g get-tbd@latest
246
+ *
247
+ * This ensures older versions fail fast rather than silently corrupting config.
248
+ * See ConfigSchema in schemas.ts and checkFormatCompatibility() in config.ts.
249
+ */
250
+ /**
251
+ * Current format version.
252
+ * Bump this ONLY for breaking changes that require migration.
253
+ */
254
+ const CURRENT_FORMAT = "f03";
255
+ /**
256
+ * Initial format version for configs that don't have tbd_format field.
257
+ */
258
+ const INITIAL_FORMAT = "f01";
259
+ /**
260
+ * Complete history of format versions with their changes.
261
+ * This serves as documentation and enables version detection.
262
+ */
263
+ const FORMAT_HISTORY = {
264
+ f01: {
265
+ introduced: "0.1.0",
266
+ description: "Initial format",
267
+ structure: {
268
+ "config.yml": "Project configuration",
269
+ "state.yml": "Local state (gitignored)",
270
+ "docs/": "Documentation cache (gitignored)",
271
+ "issues/": "Issue YAML files"
272
+ }
273
+ },
274
+ f02: {
275
+ introduced: "0.1.5",
276
+ description: "Adds configurable doc_cache",
277
+ changes: [
278
+ "Added doc_cache: key to config.yml for configurable doc sources",
279
+ "Added settings.doc_auto_sync_hours for automatic doc refresh",
280
+ "Added last_doc_sync_at to state.yml for tracking sync time"
281
+ ],
282
+ migration: "Populates default doc_cache config from bundled docs"
283
+ },
284
+ f03: {
285
+ introduced: "0.1.6",
286
+ description: "Consolidates docs_cache config structure",
287
+ changes: [
288
+ "Consolidated doc_cache: and docs: into single docs_cache: key",
289
+ "Moved doc_cache: -> docs_cache.files:",
290
+ "Moved docs.paths: -> docs_cache.lookup_path:",
291
+ "Removed separate docs: key"
292
+ ],
293
+ migration: "Migrates old config keys to new docs_cache structure"
294
+ }
295
+ };
296
+ /**
297
+ * Migrate from f01 to f02.
298
+ * - Adds tbd_format field
299
+ * - Adds doc_auto_sync_hours setting (default: 24)
300
+ * - doc_cache will be populated separately during setup (requires file system access)
301
+ */
302
+ function migrate_f01_to_f02(config) {
303
+ const changes = [];
304
+ const migrated = { ...config };
305
+ migrated.tbd_format = "f02";
306
+ changes.push("Added tbd_format: f02");
307
+ migrated.settings ??= {};
308
+ if (migrated.settings.doc_auto_sync_hours === void 0) {
309
+ migrated.settings.doc_auto_sync_hours = 24;
310
+ changes.push("Added settings.doc_auto_sync_hours: 24");
311
+ }
312
+ return {
313
+ config: migrated,
314
+ fromFormat: "f01",
315
+ toFormat: "f02",
316
+ changed: changes.length > 0,
317
+ changes
318
+ };
319
+ }
320
+ /**
321
+ * Migrate from f02 to f03.
322
+ * - Consolidates doc_cache: and docs: into docs_cache:
323
+ * - Moves doc_cache: -> docs_cache.files:
324
+ * - Moves docs.paths: -> docs_cache.lookup_path:
325
+ * - Removes separate docs: and doc_cache: keys
326
+ */
327
+ function migrate_f02_to_f03(config) {
328
+ const changes = [];
329
+ const migrated = { ...config };
330
+ migrated.tbd_format = "f03";
331
+ changes.push("Updated tbd_format: f03");
332
+ migrated.docs_cache ??= {};
333
+ if (migrated.doc_cache && Object.keys(migrated.doc_cache).length > 0) {
334
+ migrated.docs_cache.files = { ...migrated.doc_cache };
335
+ changes.push("Moved doc_cache: -> docs_cache.files:");
336
+ delete migrated.doc_cache;
337
+ }
338
+ if (migrated.docs?.paths && migrated.docs.paths.length > 0) {
339
+ migrated.docs_cache.lookup_path = [...migrated.docs.paths];
340
+ changes.push("Moved docs.paths: -> docs_cache.lookup_path:");
341
+ }
342
+ if (migrated.docs) {
343
+ delete migrated.docs;
344
+ changes.push("Removed docs: key");
345
+ }
346
+ return {
347
+ config: migrated,
348
+ fromFormat: "f02",
349
+ toFormat: "f03",
350
+ changed: changes.length > 0,
351
+ changes
352
+ };
353
+ }
354
+ /**
355
+ * Detect the format version of a config.
356
+ * Returns INITIAL_FORMAT ('f01') if no tbd_format field is present.
357
+ */
358
+ function detectFormat(config) {
359
+ const format = config.tbd_format;
360
+ if (!format) return INITIAL_FORMAT;
361
+ if (format in FORMAT_HISTORY) return format;
362
+ return CURRENT_FORMAT;
363
+ }
364
+ /**
365
+ * Check if a config needs migration.
366
+ */
367
+ function needsMigration(config) {
368
+ return detectFormat(config) !== CURRENT_FORMAT;
369
+ }
370
+ /**
371
+ * Migrate a config to the latest format version.
372
+ *
373
+ * This function applies all necessary migrations in sequence.
374
+ * It does NOT populate doc_cache - that requires file system access
375
+ * and should be done separately during setup.
376
+ *
377
+ * @param config - The raw config to migrate
378
+ * @returns Migration result with the migrated config and change log
379
+ */
380
+ function migrateToLatest(config) {
381
+ const fromFormat = detectFormat(config);
382
+ if (fromFormat === CURRENT_FORMAT) return {
383
+ config,
384
+ fromFormat,
385
+ toFormat: CURRENT_FORMAT,
386
+ changed: false,
387
+ changes: []
388
+ };
389
+ let current = config;
390
+ let currentFormat = fromFormat;
391
+ const allChanges = [];
392
+ if (currentFormat === "f01") {
393
+ const result = migrate_f01_to_f02(current);
394
+ current = result.config;
395
+ currentFormat = "f02";
396
+ allChanges.push(...result.changes);
397
+ }
398
+ if (currentFormat === "f02") {
399
+ const result = migrate_f02_to_f03(current);
400
+ current = result.config;
401
+ currentFormat = "f03";
402
+ allChanges.push(...result.changes);
403
+ }
404
+ return {
405
+ config: current,
406
+ fromFormat,
407
+ toFormat: currentFormat,
408
+ changed: allChanges.length > 0,
409
+ changes: allChanges
410
+ };
411
+ }
412
+ /**
413
+ * Check if a format version is compatible with the current tbd version.
414
+ * Future format versions are considered incompatible (would need tbd upgrade).
415
+ */
416
+ function isCompatibleFormat(format) {
417
+ const formatVersions = Object.keys(FORMAT_HISTORY);
418
+ const currentIndex = formatVersions.indexOf(CURRENT_FORMAT);
419
+ const checkIndex = formatVersions.indexOf(format);
420
+ if (checkIndex === -1) return false;
421
+ return checkIndex <= currentIndex;
422
+ }
423
+
424
+ //#endregion
425
+ //#region src/file/config.ts
426
+ /**
427
+ * Config file operations.
428
+ *
429
+ * Config is stored at .tbd/config.yml and contains project-level settings.
430
+ *
431
+ * ⚠️ FORMAT VERSIONING: See tbd-format.ts for version history and migration rules.
432
+ *
433
+ * See: tbd-design.md §2.2.2 Config File
434
+ */
435
+ /**
436
+ * Error thrown when the config format version is from a newer tbd version.
437
+ * This prevents older tbd versions from silently stripping new config fields.
438
+ */
439
+ var IncompatibleFormatError = class extends Error {
440
+ constructor(foundFormat, supportedFormat) {
441
+ 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`);
442
+ this.foundFormat = foundFormat;
443
+ this.supportedFormat = supportedFormat;
444
+ this.name = "IncompatibleFormatError";
445
+ }
446
+ };
447
+ /**
448
+ * Check if config format is compatible, throw if not.
449
+ * This prevents older tbd versions from silently stripping fields added by newer versions.
450
+ */
451
+ function checkFormatCompatibility(data) {
452
+ const format = data.tbd_format;
453
+ if (format && !isCompatibleFormat(format)) throw new IncompatibleFormatError(format, CURRENT_FORMAT);
454
+ }
455
+ /**
456
+ * Create default config for a new project.
457
+ * @param prefix - Required: the project prefix for display IDs (e.g., "proj", "myapp")
458
+ */
459
+ function createDefaultConfig(version, prefix) {
460
+ return ConfigSchema.parse({
461
+ tbd_format: CURRENT_FORMAT,
462
+ tbd_version: version,
463
+ sync: {
464
+ branch: SYNC_BRANCH,
465
+ remote: "origin"
466
+ },
467
+ display: { id_prefix: prefix },
468
+ settings: {
469
+ auto_sync: false,
470
+ doc_auto_sync_hours: 24
471
+ }
472
+ });
473
+ }
474
+ /**
475
+ * Initialize a new config file with default settings.
476
+ * Creates .tbd directory if it doesn't exist.
477
+ * @param prefix - Required: the project prefix for display IDs (e.g., "proj", "myapp")
478
+ */
479
+ async function initConfig(baseDir, version, prefix) {
480
+ await mkdir(join(baseDir, ".tbd"), { recursive: true });
481
+ const config = createDefaultConfig(version, prefix);
482
+ await writeConfig(baseDir, config);
483
+ return config;
484
+ }
485
+ /**
486
+ * Read config from file with automatic migration if needed.
487
+ *
488
+ * ⚠️ FORMAT VERSIONING: See tbd-format.ts for version history and migration rules.
489
+ *
490
+ * @throws {IncompatibleFormatError} If config is from a newer tbd version.
491
+ * @throws If config file doesn't exist or is invalid.
492
+ */
493
+ async function readConfig(baseDir) {
494
+ const data = parse(await readFile(join(baseDir, CONFIG_FILE), "utf-8"));
495
+ checkFormatCompatibility(data);
496
+ if (needsMigration(data)) {
497
+ const result = migrateToLatest(data);
498
+ return ConfigSchema.parse(result.config);
499
+ }
500
+ return ConfigSchema.parse(data);
501
+ }
502
+ /**
503
+ * Read config from file, returning migration info if a migration was applied.
504
+ * Use this when you need to know if the config was migrated.
505
+ *
506
+ * @throws {IncompatibleFormatError} If config is from a newer tbd version.
507
+ */
508
+ async function readConfigWithMigration(baseDir) {
509
+ const data = parse(await readFile(join(baseDir, CONFIG_FILE), "utf-8"));
510
+ checkFormatCompatibility(data);
511
+ if (needsMigration(data)) {
512
+ const result = migrateToLatest(data);
513
+ return {
514
+ config: ConfigSchema.parse(result.config),
515
+ migrated: result.changed,
516
+ changes: result.changes
517
+ };
518
+ }
519
+ return {
520
+ config: ConfigSchema.parse(data),
521
+ migrated: false,
522
+ changes: []
523
+ };
524
+ }
525
+ /**
526
+ * Write config to file with explanatory comments.
527
+ */
528
+ async function writeConfig(baseDir, config) {
529
+ const configPath = join(baseDir, CONFIG_FILE);
530
+ let content = stringifyYaml(sortKeys(config, CONFIG_FIELD_ORDER), {
531
+ lineWidth: 0,
532
+ sortMapEntries: false
533
+ });
534
+ 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:");
535
+ await writeFile(configPath, content);
536
+ }
537
+ /**
538
+ * Check if tbd is properly initialized in the given directory.
539
+ * Returns true only if .tbd/config.yml exists (not just a .tbd/ directory).
540
+ *
541
+ * This prevents spurious .tbd/ directories (e.g., containing only state.yml
542
+ * created by a bug) from being mistaken for tbd roots. A valid tbd root
543
+ * always has config.yml created during `tbd init`.
544
+ */
545
+ async function hasTbdDir(dir) {
546
+ const configPath = join(dir, CONFIG_FILE);
547
+ try {
548
+ await access(configPath);
549
+ return true;
550
+ } catch {
551
+ return false;
552
+ }
553
+ }
554
+ /**
555
+ * Find the tbd repository root by walking up the directory tree.
556
+ * Similar to how git finds .git/ directories.
557
+ *
558
+ * @param startDir - Directory to start searching from
559
+ * @returns The tbd root directory path, or null if not found
560
+ */
561
+ async function findTbdRoot(startDir) {
562
+ let currentDir = startDir;
563
+ const { root } = parse$1(startDir);
564
+ while (currentDir !== root) {
565
+ if (await hasTbdDir(currentDir)) return currentDir;
566
+ currentDir = dirname(currentDir);
567
+ }
568
+ if (await hasTbdDir(root)) return root;
569
+ return null;
570
+ }
571
+ /**
572
+ * Check if tbd is initialized in the given directory or any parent directory.
573
+ * Walks up the directory tree looking for .tbd/.
574
+ */
575
+ async function isInitialized(baseDir) {
576
+ return await findTbdRoot(baseDir) !== null;
577
+ }
578
+ /**
579
+ * Read local state from .tbd/state.yml
580
+ * Returns empty state if file doesn't exist.
581
+ */
582
+ async function readLocalState(baseDir) {
583
+ const statePath = join(baseDir, STATE_FILE);
584
+ try {
585
+ const data = parse(await readFile(statePath, "utf-8"));
586
+ return LocalStateSchema.parse(data ?? {});
587
+ } catch {
588
+ return {};
589
+ }
590
+ }
591
+ /**
592
+ * Write local state to .tbd/state.yml
593
+ *
594
+ * Uses `atomically` for safe writes (atomic rename, auto parent-dir creation).
595
+ * However, we intentionally guard against .tbd/ not existing: `atomically`
596
+ * would auto-create it, which is wrong if baseDir is a subdirectory rather
597
+ * than the true tbd root. Only `tbd init` (via initConfig) should create .tbd/.
598
+ */
599
+ async function writeLocalState(baseDir, state) {
600
+ const tbdDir = join(baseDir, ".tbd");
601
+ try {
602
+ await access(tbdDir);
603
+ } catch {
604
+ 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.`);
605
+ }
606
+ await writeFile(join(baseDir, STATE_FILE), stringifyYaml(sortKeys(state, LOCAL_STATE_FIELD_ORDER), {
607
+ lineWidth: 0,
608
+ sortMapEntries: false
609
+ }));
610
+ }
611
+ /**
612
+ * Update specific fields in local state (merge with existing).
613
+ */
614
+ async function updateLocalState(baseDir, updates) {
615
+ const updated = {
616
+ ...await readLocalState(baseDir),
617
+ ...updates
618
+ };
619
+ await writeLocalState(baseDir, updated);
620
+ return updated;
621
+ }
622
+ /**
623
+ * Check if the user has seen the welcome message.
624
+ */
625
+ async function hasSeenWelcome(baseDir) {
626
+ return (await readLocalState(baseDir)).welcome_seen === true;
627
+ }
628
+ /**
629
+ * Mark the welcome message as seen.
630
+ */
631
+ async function markWelcomeSeen(baseDir) {
632
+ await updateLocalState(baseDir, { welcome_seen: true });
633
+ }
634
+
635
+ //#endregion
636
+ export { isValidWorkspaceName as A, TBD_SHORTCUTS_STANDARD as C, WORKTREE_DIR as D, WORKSPACES_DIR as E, resolveDataSyncDir as M, WORKTREE_DIR_NAME as O, TBD_GUIDELINES_DIR as S, TBD_TEMPLATES_DIR as T, DEFAULT_SHORTCUT_PATHS as _, isInitialized as a, TBD_DIR as b, readConfigWithMigration as c, writeConfig as d, writeLocalState as f, DEFAULT_GUIDELINES_PATHS as g, DATA_SYNC_DIR_NAME as h, initConfig as i, resolveAtticDir as j, getWorkspaceDir as k, readLocalState as l, DATA_SYNC_DIR as m, findTbdRoot as n, markWelcomeSeen as o, CHARS_PER_TOKEN as p, hasSeenWelcome as r, readConfig as s, IncompatibleFormatError as t, updateLocalState as u, DEFAULT_TEMPLATE_PATHS as v, TBD_SHORTCUTS_SYSTEM as w, TBD_DOCS_DIR as x, SYNC_BRANCH as y };
637
+ //# sourceMappingURL=config-CmEAGaxz.mjs.map