Skip to content

Commit 3b9ac12

Browse files
dcramerclaude
andcommitted
fix(sync): Fix auto-sync and storage path bugs from v0.4.0
Fixed multiple bugs preventing auto-sync from working correctly: **Bug 1: Storage path not passed to sync service** - Sync service wasn't receiving storage path parameter - Caused sync to use wrong path (.dex instead of configured path) - Fixed: Pass storagePath to createGitHubSyncService() **Bug 2: Tilde expansion not working** - Paths like ~/.dex were used literally, creating ~/ directory - Fixed: Added expandTilde() function to FileStorage - Added tests: file-storage.test.ts **Bug 3: Config path coupled to storage location** - Project config looked in storage path instead of git root - Global storage config prevented reading repo-local config - Fixed: getProjectConfigPath() now always looks in git root - Config loading no longer depends on storage location - Added tests: config.test.ts **Bug 4: Migration for v0.4.0 bug victims** - Doctor detects tasks in literal ~/ directory - Doctor --fix migrates tasks to correct location - Startup warning alerts users to run dex doctor --fix All tests passing (518/518). Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent b3e1ec4 commit 3b9ac12

12 files changed

Lines changed: 522 additions & 117 deletions

File tree

.dex/config.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
[sync.github]
22
enabled = true
3+
4+
[sync.github.auto]
5+
on_change = true

src/bootstrap.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,10 @@ export function createSyncService(
104104
const config = loadConfig({ storagePath, configPath: cliConfigPath });
105105
const githubConfig = config.sync?.github ?? null;
106106
return {
107-
syncService: createGitHubSyncService(githubConfig ?? undefined),
107+
syncService: createGitHubSyncService(
108+
githubConfig ?? undefined,
109+
storagePath,
110+
),
108111
syncConfig: githubConfig,
109112
};
110113
}

src/cli/config.test.ts

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,33 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
22
import * as fs from "node:fs";
33
import * as path from "node:path";
44
import * as os from "node:os";
5+
import { execSync } from "node:child_process";
56
import { configCommand } from "./config.js";
67
import { captureOutput, CapturedOutput } from "./test-helpers.js";
78

89
describe("config command", () => {
910
let output: CapturedOutput;
1011
let mockExit: ReturnType<typeof vi.spyOn>;
1112
let tempDir: string;
12-
let tempStorageDir: string;
13+
let tempGitDir: string;
14+
let originalCwd: string;
1315

1416
beforeEach(() => {
17+
originalCwd = process.cwd();
18+
19+
// Create temp dir for global config
1520
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "dex-config-test-"));
16-
tempStorageDir = fs.mkdtempSync(
17-
path.join(os.tmpdir(), "dex-config-storage-"),
18-
);
21+
22+
// Create temp git repo for project config tests
23+
tempGitDir = fs.mkdtempSync(path.join(os.tmpdir(), "dex-config-git-"));
24+
execSync("git init", { cwd: tempGitDir, stdio: "ignore" });
25+
26+
// Create .dex directory in git repo
27+
fs.mkdirSync(path.join(tempGitDir, ".dex"), { recursive: true });
28+
29+
// Change to git repo for tests that need it
30+
process.chdir(tempGitDir);
31+
1932
output = captureOutput();
2033
mockExit = vi.spyOn(process, "exit").mockImplementation((() => {
2134
throw new Error("process.exit called");
@@ -26,12 +39,13 @@ describe("config command", () => {
2639
});
2740

2841
afterEach(() => {
42+
process.chdir(originalCwd);
2943
output.restore();
3044
mockExit.mockRestore();
3145
delete process.env.DEX_HOME;
3246

3347
fs.rmSync(tempDir, { recursive: true, force: true });
34-
fs.rmSync(tempStorageDir, { recursive: true, force: true });
48+
fs.rmSync(tempGitDir, { recursive: true, force: true });
3549
});
3650

3751
describe("--help", () => {
@@ -188,24 +202,24 @@ describe("config command", () => {
188202
"[sync.github]\nenabled = false\n",
189203
);
190204
fs.writeFileSync(
191-
path.join(tempStorageDir, "config.toml"),
205+
path.join(tempGitDir, ".dex", "config.toml"),
192206
"[sync.github]\nenabled = true\n",
193207
);
194208

195-
await configCommand(["--list"], { storagePath: tempStorageDir });
209+
await configCommand(["--list"], { storagePath: tempGitDir });
196210
const out = output.stdout.join("\n");
197211
expect(out).toContain("sync.github.enabled = true");
198212
expect(out).toContain("[local]");
199213
});
200214
});
201215

202216
describe("--local", () => {
203-
it("writes to project config file", async () => {
217+
it("writes to project config file in git root", async () => {
204218
await configCommand(["--local", "sync.github.enabled=true"], {
205-
storagePath: tempStorageDir,
219+
storagePath: tempGitDir,
206220
});
207221

208-
const projectConfig = path.join(tempStorageDir, "config.toml");
222+
const projectConfig = path.join(tempGitDir, ".dex", "config.toml");
209223
expect(fs.existsSync(projectConfig)).toBe(true);
210224
const content = fs.readFileSync(projectConfig, "utf-8");
211225
expect(content).toContain("enabled = true");
@@ -228,7 +242,7 @@ describe("config command", () => {
228242
it("fails when both are specified", async () => {
229243
await expect(
230244
configCommand(["--global", "--local", "sync.github.enabled=true"], {
231-
storagePath: tempStorageDir,
245+
storagePath: tempGitDir,
232246
}),
233247
).rejects.toThrow("process.exit");
234248
const err = output.stderr.join("\n");
@@ -243,12 +257,12 @@ describe("config command", () => {
243257
"[sync.github]\nenabled = false\n",
244258
);
245259
fs.writeFileSync(
246-
path.join(tempStorageDir, "config.toml"),
260+
path.join(tempGitDir, ".dex", "config.toml"),
247261
"[sync.github]\nenabled = true\n",
248262
);
249263

250264
await configCommand(["sync.github.enabled"], {
251-
storagePath: tempStorageDir,
265+
storagePath: tempGitDir,
252266
});
253267
const out = output.stdout.join("\n");
254268
expect(out).toBe("true");
@@ -260,12 +274,12 @@ describe("config command", () => {
260274
'[sync.github]\nlabel_prefix = "global-prefix"\n',
261275
);
262276
fs.writeFileSync(
263-
path.join(tempStorageDir, "config.toml"),
277+
path.join(tempGitDir, "config.toml"),
264278
"[sync.github]\nenabled = true\n",
265279
);
266280

267281
await configCommand(["sync.github.label_prefix"], {
268-
storagePath: tempStorageDir,
282+
storagePath: tempGitDir,
269283
});
270284
const out = output.stdout.join("\n");
271285
expect(out).toBe("global-prefix");

src/cli/config.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,14 @@ ${colors.bold}EXAMPLES:${colors.reset}
270270
);
271271
process.exit(1);
272272
}
273-
configPath = getProjectConfigPath(options.storagePath);
273+
const projectPath = getProjectConfigPath();
274+
if (!projectPath) {
275+
console.error(
276+
`${colors.dim}Run "dex init" to initialize a git repository or use --global${colors.reset}`,
277+
);
278+
process.exit(1);
279+
}
280+
configPath = projectPath;
274281
configLabel = "local";
275282
} else {
276283
// Default to global
@@ -281,9 +288,8 @@ ${colors.bold}EXAMPLES:${colors.reset}
281288
// Handle --list
282289
if (listAll) {
283290
const globalConfig = readConfigFile(getConfigPath());
284-
const localConfig = options.storagePath
285-
? readConfigFile(getProjectConfigPath(options.storagePath))
286-
: {};
291+
const projectPath = getProjectConfigPath();
292+
const localConfig = projectPath ? readConfigFile(projectPath) : {};
287293

288294
console.log(`${colors.bold}Configuration:${colors.reset}`);
289295
console.log();
@@ -356,9 +362,8 @@ ${colors.bold}EXAMPLES:${colors.reset}
356362

357363
// Read from both configs and show effective value
358364
const globalConfig = readConfigFile(getConfigPath());
359-
const localConfig = options.storagePath
360-
? readConfigFile(getProjectConfigPath(options.storagePath))
361-
: {};
365+
const projectPath = getProjectConfigPath();
366+
const localConfig = projectPath ? readConfigFile(projectPath) : {};
362367

363368
const globalValue = getNestedValue(globalConfig, input);
364369
const localValue = getNestedValue(localConfig, input);

src/cli/doctor.test.ts

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,23 @@ import * as fs from "node:fs";
33
import * as path from "node:path";
44
import { FileStorage } from "../core/storage/index.js";
55
import { runCli } from "./index.js";
6-
import { captureOutput, createTempStorage, CapturedOutput } from "./test-helpers.js";
6+
import {
7+
captureOutput,
8+
createTempGitStorage,
9+
CapturedOutput,
10+
} from "./test-helpers.js";
711

812
describe("doctor command", () => {
913
let storage: FileStorage;
14+
let gitRoot: string;
1015
let cleanup: () => void;
1116
let output: CapturedOutput;
1217
let mockExit: ReturnType<typeof vi.spyOn>;
1318

1419
beforeEach(() => {
15-
const temp = createTempStorage();
20+
const temp = createTempGitStorage();
1621
storage = temp.storage;
22+
gitRoot = temp.gitRoot;
1723
cleanup = temp.cleanup;
1824
output = captureOutput();
1925
mockExit = vi.spyOn(process, "exit").mockImplementation((() => {
@@ -53,21 +59,24 @@ describe("doctor command", () => {
5359
const taskPath = path.join(storagePath, "tasks", `${taskId}.json`);
5460

5561
fs.mkdirSync(path.dirname(taskPath), { recursive: true });
56-
fs.writeFileSync(taskPath, JSON.stringify({
57-
id: taskId,
58-
parent_id: "nonexistent",
59-
description: "Test task",
60-
context: "ctx",
61-
priority: 1,
62-
completed: false,
63-
result: null,
64-
blockedBy: [],
65-
blocks: [],
66-
children: [],
67-
created_at: new Date().toISOString(),
68-
updated_at: new Date().toISOString(),
69-
completed_at: null,
70-
}));
62+
fs.writeFileSync(
63+
taskPath,
64+
JSON.stringify({
65+
id: taskId,
66+
parent_id: "nonexistent",
67+
description: "Test task",
68+
context: "ctx",
69+
priority: 1,
70+
completed: false,
71+
result: null,
72+
blockedBy: [],
73+
blocks: [],
74+
children: [],
75+
created_at: new Date().toISOString(),
76+
updated_at: new Date().toISOString(),
77+
completed_at: null,
78+
}),
79+
);
7180

7281
await runCli(["doctor"], { storage });
7382

@@ -78,12 +87,14 @@ describe("doctor command", () => {
7887

7988
it("detects missing auto-sync config when github sync is enabled", async () => {
8089
// Create a config file with github sync enabled but no auto section
81-
const storagePath = storage.getIdentifier();
82-
const configPath = path.join(storagePath, "config.toml");
90+
const configPath = path.join(gitRoot, ".dex", "config.toml");
8391

84-
fs.writeFileSync(configPath, `[sync.github]
92+
fs.writeFileSync(
93+
configPath,
94+
`[sync.github]
8595
enabled = true
86-
`);
96+
`,
97+
);
8798

8899
await runCli(["doctor"], { storage });
89100

@@ -94,12 +105,14 @@ enabled = true
94105

95106
it("fixes missing auto-sync config with --fix", async () => {
96107
// Create a config file with github sync enabled but no auto section
97-
const storagePath = storage.getIdentifier();
98-
const configPath = path.join(storagePath, "config.toml");
108+
const configPath = path.join(gitRoot, ".dex", "config.toml");
99109

100-
fs.writeFileSync(configPath, `[sync.github]
110+
fs.writeFileSync(
111+
configPath,
112+
`[sync.github]
101113
enabled = true
102-
`);
114+
`,
115+
);
103116

104117
await runCli(["doctor", "--fix"], { storage });
105118

@@ -114,15 +127,17 @@ enabled = true
114127

115128
it("does not warn about auto-sync when it's already present", async () => {
116129
// Create a config file with github sync and auto section
117-
const storagePath = storage.getIdentifier();
118-
const configPath = path.join(storagePath, "config.toml");
130+
const configPath = path.join(gitRoot, ".dex", "config.toml");
119131

120-
fs.writeFileSync(configPath, `[sync.github]
132+
fs.writeFileSync(
133+
configPath,
134+
`[sync.github]
121135
enabled = true
122136
123137
[sync.github.auto]
124138
on_change = false
125-
`);
139+
`,
140+
);
126141

127142
await runCli(["doctor"], { storage });
128143

0 commit comments

Comments
 (0)