tlc-claude-code 2.4.2 → 2.4.3

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,221 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { dispatchToCodex, resumeSession } from './codex-orchestrator.js';
3
+
4
+ function createMockSpawn({
5
+ stdoutChunks = [],
6
+ stderrChunks = [],
7
+ exitCode = 0,
8
+ delay = 0,
9
+ error = null,
10
+ } = {}) {
11
+ return vi.fn(() => {
12
+ const listeners = {};
13
+ const stdinChunks = [];
14
+
15
+ const proc = {
16
+ stdin: {
17
+ write(data) { stdinChunks.push(data); },
18
+ end() { stdinChunks.push(null); },
19
+ on(event, cb) { listeners[`stdin:${event}`] = cb; },
20
+ },
21
+ stdout: {
22
+ on(event, cb) { listeners[`stdout:${event}`] = cb; },
23
+ },
24
+ stderr: {
25
+ on(event, cb) { listeners[`stderr:${event}`] = cb; },
26
+ },
27
+ on(event, cb) { listeners[event] = cb; },
28
+ kill: vi.fn(() => {
29
+ listeners._killed = true;
30
+ }),
31
+ _stdinChunks: stdinChunks,
32
+ _listeners: listeners,
33
+ };
34
+
35
+ setTimeout(() => {
36
+ if (error) {
37
+ listeners.error?.(error);
38
+ return;
39
+ }
40
+
41
+ stdoutChunks.forEach((chunk) => {
42
+ listeners['stdout:data']?.(Buffer.from(chunk));
43
+ });
44
+
45
+ stderrChunks.forEach((chunk) => {
46
+ listeners['stderr:data']?.(Buffer.from(chunk));
47
+ });
48
+
49
+ if (!listeners._killed) {
50
+ listeners.close?.(exitCode);
51
+ }
52
+ }, delay);
53
+
54
+ return proc;
55
+ });
56
+ }
57
+
58
+ describe('codex-orchestrator', () => {
59
+ describe('dispatchToCodex', () => {
60
+ it('builds correct command args', async () => {
61
+ const spawn = createMockSpawn();
62
+
63
+ await dispatchToCodex({
64
+ worktreePath: '/tmp/worktree',
65
+ prompt: 'Implement the task',
66
+ spawn,
67
+ });
68
+
69
+ expect(spawn).toHaveBeenCalledWith(
70
+ 'codex',
71
+ ['exec', '--json', '--full-auto', '-C', '/tmp/worktree'],
72
+ {}
73
+ );
74
+ });
75
+
76
+ it('pipes prompt via stdin', async () => {
77
+ const spawn = createMockSpawn();
78
+
79
+ await dispatchToCodex({
80
+ worktreePath: '/tmp/worktree',
81
+ prompt: 'Review this change',
82
+ spawn,
83
+ });
84
+
85
+ const proc = spawn.mock.results[0].value;
86
+ expect(proc._stdinChunks).toContain('Review this change');
87
+ expect(proc._stdinChunks).toContain(null);
88
+ });
89
+
90
+ it('extracts thread_id from JSON stream', async () => {
91
+ const spawn = createMockSpawn({
92
+ stdoutChunks: [
93
+ '{"type":"status","message":"booting"}\n',
94
+ '{"type":"thread.started","thread_id":"thread_123"}\n',
95
+ '{"type":"message.delta","delta":"done"}\n',
96
+ ],
97
+ });
98
+
99
+ const result = await dispatchToCodex({
100
+ worktreePath: '/tmp/worktree',
101
+ prompt: 'Do work',
102
+ spawn,
103
+ });
104
+
105
+ expect(result.threadId).toBe('thread_123');
106
+ expect(result.stdout).toContain('"thread_id":"thread_123"');
107
+ expect(result.exitCode).toBe(0);
108
+ });
109
+
110
+ it('ignores invalid JSON lines and still extracts thread_id', async () => {
111
+ const spawn = createMockSpawn({
112
+ stdoutChunks: [
113
+ 'not-json\n',
114
+ '{"type":"thread.started","thread_id":"thread_abc"}\n',
115
+ ],
116
+ });
117
+
118
+ const result = await dispatchToCodex({
119
+ worktreePath: '/tmp/worktree',
120
+ prompt: 'Do work',
121
+ spawn,
122
+ });
123
+
124
+ expect(result.threadId).toBe('thread_abc');
125
+ });
126
+
127
+ it('timeout kills process and returns error', async () => {
128
+ const spawn = createMockSpawn({ delay: 1000 });
129
+
130
+ const result = await dispatchToCodex({
131
+ worktreePath: '/tmp/worktree',
132
+ prompt: 'Slow task',
133
+ timeout: 25,
134
+ spawn,
135
+ });
136
+
137
+ const proc = spawn.mock.results[0].value;
138
+ expect(proc.kill).toHaveBeenCalledTimes(1);
139
+ expect(result.exitCode).toBe(-1);
140
+ expect(result.error).toEqual(
141
+ expect.objectContaining({ code: 'PROCESS_TIMEOUT' })
142
+ );
143
+ expect(result.stderr).toContain('timed out');
144
+ });
145
+
146
+ it('missing codex CLI returns structured error', async () => {
147
+ const spawn = createMockSpawn({
148
+ error: Object.assign(new Error('spawn codex ENOENT'), { code: 'ENOENT' }),
149
+ });
150
+
151
+ const result = await dispatchToCodex({
152
+ worktreePath: '/tmp/worktree',
153
+ prompt: 'Task',
154
+ spawn,
155
+ });
156
+
157
+ expect(result.exitCode).toBe(-1);
158
+ expect(result.threadId).toBeNull();
159
+ expect(result.error).toEqual({
160
+ code: 'CODEX_CLI_NOT_FOUND',
161
+ message: 'Codex CLI is not installed or not available on PATH',
162
+ });
163
+ expect(result.stderr).toContain('ENOENT');
164
+ });
165
+
166
+ it('captures non-zero exit without throwing', async () => {
167
+ const spawn = createMockSpawn({
168
+ stderrChunks: ['process failed'],
169
+ exitCode: 17,
170
+ });
171
+
172
+ const result = await dispatchToCodex({
173
+ worktreePath: '/tmp/worktree',
174
+ prompt: 'Task',
175
+ spawn,
176
+ });
177
+
178
+ expect(result.exitCode).toBe(17);
179
+ expect(result.stderr).toBe('process failed');
180
+ expect(result.error).toBeNull();
181
+ });
182
+ });
183
+
184
+ describe('resumeSession', () => {
185
+ it('builds correct resume command', async () => {
186
+ const spawn = createMockSpawn();
187
+
188
+ await resumeSession({
189
+ threadId: 'thread_123',
190
+ prompt: 'Continue',
191
+ spawn,
192
+ });
193
+
194
+ expect(spawn).toHaveBeenCalledWith(
195
+ 'codex',
196
+ ['exec', 'resume', 'thread_123', 'Continue'],
197
+ {}
198
+ );
199
+ });
200
+
201
+ it('passes prompt correctly', async () => {
202
+ const spawn = createMockSpawn({
203
+ stdoutChunks: ['resumed'],
204
+ });
205
+
206
+ const result = await resumeSession({
207
+ threadId: 'thread_123',
208
+ prompt: 'Follow up task',
209
+ spawn,
210
+ });
211
+
212
+ expect(spawn).toHaveBeenCalledWith(
213
+ 'codex',
214
+ ['exec', 'resume', 'thread_123', 'Follow up task'],
215
+ {}
216
+ );
217
+ expect(result.stdout).toBe('resumed');
218
+ expect(result.exitCode).toBe(0);
219
+ });
220
+ });
221
+ });
@@ -0,0 +1,61 @@
1
+ const path = require('path');
2
+
3
+ const DEP_DIRS = ['node_modules', 'vendor', '.venv'];
4
+
5
+ function pathExists(fs, targetPath) {
6
+ return fs.existsSync(targetPath);
7
+ }
8
+
9
+ function readExistingLinkTarget(fs, linkPath) {
10
+ try {
11
+ return path.resolve(path.dirname(linkPath), fs.readlinkSync(linkPath));
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+
17
+ function symlinkDeps({ worktreePath, mainRepoPath, fs = require('fs') }) {
18
+ const linked = [];
19
+ const skipped = [];
20
+ const symlinkType = process.platform === 'win32' ? 'junction' : 'dir';
21
+
22
+ for (const depDir of DEP_DIRS) {
23
+ const sourcePath = path.join(mainRepoPath, depDir);
24
+ const targetPath = path.join(worktreePath, depDir);
25
+
26
+ if (!pathExists(fs, sourcePath)) {
27
+ skipped.push(depDir);
28
+ continue;
29
+ }
30
+
31
+ let targetStats = null;
32
+ try {
33
+ targetStats = fs.lstatSync(targetPath);
34
+ } catch {
35
+ targetStats = null;
36
+ }
37
+
38
+ if (targetStats) {
39
+ if (targetStats.isSymbolicLink()) {
40
+ if (readExistingLinkTarget(fs, targetPath) === sourcePath) {
41
+ skipped.push(depDir);
42
+ continue;
43
+ }
44
+
45
+ fs.rmSync(targetPath, { recursive: true, force: true });
46
+ } else {
47
+ skipped.push(depDir);
48
+ continue;
49
+ }
50
+ }
51
+
52
+ fs.symlinkSync(sourcePath, targetPath, symlinkType);
53
+ linked.push(depDir);
54
+ }
55
+
56
+ return { linked, skipped };
57
+ }
58
+
59
+ module.exports = {
60
+ symlinkDeps,
61
+ };
@@ -0,0 +1,174 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+ const crypto = require('crypto');
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ const { symlinkDeps } = require('./dep-linker.js');
8
+
9
+ const tempPaths = [];
10
+
11
+ function makeTempDir() {
12
+ const dirPath = path.join(os.tmpdir(), `dep-linker-${crypto.randomUUID()}`);
13
+ fs.mkdirSync(dirPath, { recursive: true });
14
+ tempPaths.push(dirPath);
15
+ return dirPath;
16
+ }
17
+
18
+ function createRepoPair() {
19
+ const rootPath = makeTempDir();
20
+ const mainRepoPath = path.join(rootPath, 'main');
21
+ const worktreePath = path.join(rootPath, 'worktree');
22
+
23
+ fs.mkdirSync(mainRepoPath, { recursive: true });
24
+ fs.mkdirSync(worktreePath, { recursive: true });
25
+
26
+ return { mainRepoPath, worktreePath };
27
+ }
28
+
29
+ function makeDir(targetPath) {
30
+ fs.mkdirSync(targetPath, { recursive: true });
31
+ }
32
+
33
+ function readLinkTarget(linkPath) {
34
+ return path.resolve(path.dirname(linkPath), fs.readlinkSync(linkPath));
35
+ }
36
+
37
+ afterEach(() => {
38
+ while (tempPaths.length > 0) {
39
+ fs.rmSync(tempPaths.pop(), { recursive: true, force: true });
40
+ }
41
+ });
42
+
43
+ describe('dep-linker', () => {
44
+ it('symlinks node_modules when exists in main', () => {
45
+ const { mainRepoPath, worktreePath } = createRepoPair();
46
+ const sourcePath = path.join(mainRepoPath, 'node_modules');
47
+ const linkPath = path.join(worktreePath, 'node_modules');
48
+
49
+ makeDir(sourcePath);
50
+
51
+ const result = symlinkDeps({ worktreePath, mainRepoPath, fs });
52
+
53
+ expect(result.linked).toEqual(['node_modules']);
54
+ expect(result.skipped).toEqual(['vendor', '.venv']);
55
+ expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
56
+ expect(readLinkTarget(linkPath)).toBe(sourcePath);
57
+ });
58
+
59
+ it('skips node_modules when missing from main', () => {
60
+ const { mainRepoPath, worktreePath } = createRepoPair();
61
+
62
+ const result = symlinkDeps({ worktreePath, mainRepoPath, fs });
63
+
64
+ expect(result.linked).toEqual([]);
65
+ expect(result.skipped).toEqual(['node_modules', 'vendor', '.venv']);
66
+ expect(fs.existsSync(path.join(worktreePath, 'node_modules'))).toBe(false);
67
+ });
68
+
69
+ it('symlinks vendor when exists', () => {
70
+ const { mainRepoPath, worktreePath } = createRepoPair();
71
+ const sourcePath = path.join(mainRepoPath, 'vendor');
72
+ const linkPath = path.join(worktreePath, 'vendor');
73
+
74
+ makeDir(sourcePath);
75
+
76
+ const result = symlinkDeps({ worktreePath, mainRepoPath, fs });
77
+
78
+ expect(result.linked).toEqual(['vendor']);
79
+ expect(result.skipped).toEqual(['node_modules', '.venv']);
80
+ expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
81
+ expect(readLinkTarget(linkPath)).toBe(sourcePath);
82
+ });
83
+
84
+ it('symlinks .venv when exists', () => {
85
+ const { mainRepoPath, worktreePath } = createRepoPair();
86
+ const sourcePath = path.join(mainRepoPath, '.venv');
87
+ const linkPath = path.join(worktreePath, '.venv');
88
+
89
+ makeDir(sourcePath);
90
+
91
+ const result = symlinkDeps({ worktreePath, mainRepoPath, fs });
92
+
93
+ expect(result.linked).toEqual(['.venv']);
94
+ expect(result.skipped).toEqual(['node_modules', 'vendor']);
95
+ expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
96
+ expect(readLinkTarget(linkPath)).toBe(sourcePath);
97
+ });
98
+
99
+ it('returns correct linked and skipped arrays', () => {
100
+ const { mainRepoPath, worktreePath } = createRepoPair();
101
+
102
+ makeDir(path.join(mainRepoPath, 'node_modules'));
103
+ makeDir(path.join(mainRepoPath, 'vendor'));
104
+
105
+ expect(symlinkDeps({ worktreePath, mainRepoPath, fs })).toEqual({
106
+ linked: ['node_modules', 'vendor'],
107
+ skipped: ['.venv'],
108
+ });
109
+ });
110
+
111
+ it('skips when worktree already has real directory', () => {
112
+ const { mainRepoPath, worktreePath } = createRepoPair();
113
+ const existingPath = path.join(worktreePath, 'node_modules');
114
+
115
+ makeDir(path.join(mainRepoPath, 'node_modules'));
116
+ makeDir(existingPath);
117
+ fs.writeFileSync(path.join(existingPath, 'keep.txt'), 'present');
118
+
119
+ const result = symlinkDeps({ worktreePath, mainRepoPath, fs });
120
+
121
+ expect(result.linked).toEqual([]);
122
+ expect(result.skipped).toEqual(['node_modules', 'vendor', '.venv']);
123
+ expect(fs.lstatSync(existingPath).isDirectory()).toBe(true);
124
+ expect(fs.lstatSync(existingPath).isSymbolicLink()).toBe(false);
125
+ expect(fs.readFileSync(path.join(existingPath, 'keep.txt'), 'utf8')).toBe('present');
126
+ });
127
+
128
+ it('skips when correct symlink already exists', () => {
129
+ const { mainRepoPath, worktreePath } = createRepoPair();
130
+ const sourcePath = path.join(mainRepoPath, 'node_modules');
131
+ const linkPath = path.join(worktreePath, 'node_modules');
132
+ const symlinkType = process.platform === 'win32' ? 'junction' : 'dir';
133
+
134
+ makeDir(sourcePath);
135
+ fs.symlinkSync(sourcePath, linkPath, symlinkType);
136
+
137
+ const result = symlinkDeps({ worktreePath, mainRepoPath, fs });
138
+
139
+ expect(result.linked).toEqual([]);
140
+ expect(result.skipped).toEqual(['node_modules', 'vendor', '.venv']);
141
+ expect(readLinkTarget(linkPath)).toBe(sourcePath);
142
+ });
143
+
144
+ it('handles all three dirs in one call', () => {
145
+ const { mainRepoPath, worktreePath } = createRepoPair();
146
+
147
+ makeDir(path.join(mainRepoPath, 'node_modules'));
148
+ makeDir(path.join(mainRepoPath, 'vendor'));
149
+ makeDir(path.join(mainRepoPath, '.venv'));
150
+
151
+ expect(symlinkDeps({ worktreePath, mainRepoPath, fs })).toEqual({
152
+ linked: ['node_modules', 'vendor', '.venv'],
153
+ skipped: [],
154
+ });
155
+
156
+ expect(fs.lstatSync(path.join(worktreePath, 'node_modules')).isSymbolicLink()).toBe(true);
157
+ expect(fs.lstatSync(path.join(worktreePath, 'vendor')).isSymbolicLink()).toBe(true);
158
+ expect(fs.lstatSync(path.join(worktreePath, '.venv')).isSymbolicLink()).toBe(true);
159
+ });
160
+
161
+ it('works with empty worktree dir', () => {
162
+ const { mainRepoPath, worktreePath } = createRepoPair();
163
+
164
+ makeDir(path.join(mainRepoPath, 'node_modules'));
165
+
166
+ expect(fs.readdirSync(worktreePath)).toEqual([]);
167
+
168
+ const result = symlinkDeps({ worktreePath, mainRepoPath, fs });
169
+
170
+ expect(result.linked).toEqual(['node_modules']);
171
+ expect(result.skipped).toEqual(['vendor', '.venv']);
172
+ expect(fs.lstatSync(path.join(worktreePath, 'node_modules')).isSymbolicLink()).toBe(true);
173
+ });
174
+ });
@@ -20,6 +20,10 @@ export const defaultConfig = {
20
20
  devserver: { url: null, queue: { maxConcurrent: 3, timeout: 120000 } },
21
21
  };
22
22
 
23
+ function isMissingConfigError(error) {
24
+ return error?.code === 'ENOENT' || error?.message?.includes('ENOENT');
25
+ }
26
+
23
27
  export async function loadRouterConfig(options = {}) {
24
28
  try {
25
29
  let content;
@@ -38,7 +42,13 @@ export async function loadRouterConfig(options = {}) {
38
42
  capabilities: { ...defaultConfig.capabilities, ...routerConfig.capabilities },
39
43
  devserver: { ...defaultConfig.devserver, ...routerConfig.devserver },
40
44
  };
41
- } catch {
45
+ } catch (error) {
46
+ if (!isMissingConfigError(error)) {
47
+ const message = error instanceof SyntaxError
48
+ ? 'Failed to parse .tlc.json router config; using defaults'
49
+ : `Failed to load .tlc.json router config; using defaults (${error?.message || 'unknown error'})`;
50
+ console.warn(`[TLC WARNING] ${message}`);
51
+ }
42
52
  return { ...defaultConfig };
43
53
  }
44
54
  }
@@ -75,8 +85,13 @@ export async function saveRouterConfig(config, options = {}) {
75
85
  try {
76
86
  const content = await readFile(configPath, 'utf-8');
77
87
  existing = JSON.parse(content);
78
- } catch {
79
- // New file
88
+ } catch (error) {
89
+ if (!isMissingConfigError(error)) {
90
+ const message = error instanceof SyntaxError
91
+ ? 'Failed to parse existing .tlc.json router config; overwriting router section'
92
+ : `Failed to load existing .tlc.json router config; overwriting router section (${error?.message || 'unknown error'})`;
93
+ console.warn(`[TLC WARNING] ${message}`);
94
+ }
80
95
  }
81
96
 
82
97
  existing.router = config;
@@ -1,4 +1,7 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
1
+ import { describe, it, expect, vi, afterEach } from 'vitest';
2
+ import { mkdtemp, rm, writeFile } from 'fs/promises';
3
+ import { tmpdir } from 'os';
4
+ import { join } from 'path';
2
5
  import {
3
6
  loadRouterConfig,
4
7
  validateConfig,
@@ -9,6 +12,13 @@ import {
9
12
  } from './router-config.js';
10
13
 
11
14
  describe('Router Config', () => {
15
+ const originalCwd = process.cwd();
16
+
17
+ afterEach(() => {
18
+ vi.restoreAllMocks();
19
+ process.chdir(originalCwd);
20
+ });
21
+
12
22
  describe('loadRouterConfig', () => {
13
23
  it('reads from .tlc.json', async () => {
14
24
  const config = await loadRouterConfig({ _readFile: vi.fn().mockResolvedValue(JSON.stringify({
@@ -28,6 +38,32 @@ describe('Router Config', () => {
28
38
  const config = await loadRouterConfig({ _readFile: vi.fn().mockResolvedValue('{}') });
29
39
  expect(config.providers).toBeDefined();
30
40
  });
41
+
42
+ it('warns when router config JSON is invalid', async () => {
43
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
44
+
45
+ const config = await loadRouterConfig({
46
+ _readFile: vi.fn().mockResolvedValue('{invalid json'),
47
+ });
48
+
49
+ expect(config).toEqual({ ...defaultConfig });
50
+ expect(warnSpy).toHaveBeenCalledWith(
51
+ '[TLC WARNING] Failed to parse .tlc.json router config; using defaults'
52
+ );
53
+ });
54
+
55
+ it('stays silent when router config file is missing', async () => {
56
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
57
+ const error = new Error('ENOENT');
58
+ error.code = 'ENOENT';
59
+
60
+ const config = await loadRouterConfig({
61
+ _readFile: vi.fn().mockRejectedValue(error),
62
+ });
63
+
64
+ expect(config).toEqual({ ...defaultConfig });
65
+ expect(warnSpy).not.toHaveBeenCalled();
66
+ });
31
67
  });
32
68
 
33
69
  describe('validateConfig', () => {
@@ -79,5 +115,25 @@ describe('Router Config', () => {
79
115
  await saveRouterConfig({ providers: {} }, { _writeFile: writeFile });
80
116
  expect(writeFile).toHaveBeenCalled();
81
117
  });
118
+
119
+ it('warns when existing router config JSON is invalid', async () => {
120
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
121
+ const tempDir = await mkdtemp(join(tmpdir(), 'router-config-test-'));
122
+ await writeFile(join(tempDir, '.tlc.json'), '{invalid json');
123
+ process.chdir(tempDir);
124
+
125
+ await saveRouterConfig(
126
+ { providers: {} },
127
+ {
128
+ _writeFile: vi.fn().mockResolvedValue(undefined),
129
+ },
130
+ );
131
+
132
+ expect(warnSpy).toHaveBeenCalledWith(
133
+ '[TLC WARNING] Failed to parse existing .tlc.json router config; overwriting router section'
134
+ );
135
+
136
+ await rm(tempDir, { recursive: true, force: true });
137
+ });
82
138
  });
83
139
  });
@@ -0,0 +1,34 @@
1
+ const {
2
+ resolveRouting,
3
+ loadPersonalConfig,
4
+ loadProjectOverride,
5
+ SHIPPED_DEFAULTS,
6
+ ROUTABLE_COMMANDS,
7
+ } = require('../task-router-config.js');
8
+ const { dispatch, buildProviderCommand } = require('../cli-dispatcher.js');
9
+ const { routeCommand } = require('../command-router.js');
10
+ const {
11
+ showRouting,
12
+ showProviders,
13
+ savePersonalRouting,
14
+ formatRoutingTable,
15
+ formatProviderList,
16
+ } = require('../routing-command.js');
17
+ const { generatePreamble } = require('../routing-preamble.js');
18
+
19
+ module.exports = {
20
+ resolveRouting,
21
+ loadPersonalConfig,
22
+ loadProjectOverride,
23
+ SHIPPED_DEFAULTS,
24
+ ROUTABLE_COMMANDS,
25
+ dispatch,
26
+ buildProviderCommand,
27
+ routeCommand,
28
+ showRouting,
29
+ showProviders,
30
+ savePersonalRouting,
31
+ formatRoutingTable,
32
+ formatProviderList,
33
+ generatePreamble,
34
+ };
@@ -0,0 +1,33 @@
1
+ import { createRequire } from 'module';
2
+ import { describe, it, expect } from 'vitest';
3
+
4
+ const require = createRequire(import.meta.url);
5
+ const routing = require('./index.js');
6
+ const taskRouterConfig = require('../task-router-config.js');
7
+ const cliDispatcher = require('../cli-dispatcher.js');
8
+ const commandRouter = require('../command-router.js');
9
+ const routingCommand = require('../routing-command.js');
10
+ const routingPreamble = require('../routing-preamble.js');
11
+
12
+ describe('routing barrel exports', () => {
13
+ it('re-exports the full routing surface from the flat modules', () => {
14
+ expect(routing.resolveRouting).toBe(taskRouterConfig.resolveRouting);
15
+ expect(routing.loadPersonalConfig).toBe(taskRouterConfig.loadPersonalConfig);
16
+ expect(routing.loadProjectOverride).toBe(taskRouterConfig.loadProjectOverride);
17
+ expect(routing.SHIPPED_DEFAULTS).toBe(taskRouterConfig.SHIPPED_DEFAULTS);
18
+ expect(routing.ROUTABLE_COMMANDS).toBe(taskRouterConfig.ROUTABLE_COMMANDS);
19
+
20
+ expect(routing.dispatch).toBe(cliDispatcher.dispatch);
21
+ expect(routing.buildProviderCommand).toBe(cliDispatcher.buildProviderCommand);
22
+
23
+ expect(routing.routeCommand).toBe(commandRouter.routeCommand);
24
+
25
+ expect(routing.showRouting).toBe(routingCommand.showRouting);
26
+ expect(routing.showProviders).toBe(routingCommand.showProviders);
27
+ expect(routing.savePersonalRouting).toBe(routingCommand.savePersonalRouting);
28
+ expect(routing.formatRoutingTable).toBe(routingCommand.formatRoutingTable);
29
+ expect(routing.formatProviderList).toBe(routingCommand.formatProviderList);
30
+
31
+ expect(routing.generatePreamble).toBe(routingPreamble.generatePreamble);
32
+ });
33
+ });
@@ -7,6 +7,10 @@
7
7
 
8
8
  const path = require('path');
9
9
 
10
+ function isMissingConfigError(error) {
11
+ return error?.code === 'ENOENT' || error?.message?.includes('ENOENT');
12
+ }
13
+
10
14
  /**
11
15
  * Show effective routing table after precedence merge.
12
16
  * @param {Object} deps - Injected dependencies
@@ -70,8 +74,13 @@ function savePersonalRouting({ routing, homeDir, fs }) {
70
74
  try {
71
75
  const content = fs.readFileSync(configPath, 'utf-8');
72
76
  existing = JSON.parse(content);
73
- } catch {
74
- // No existing config, start fresh
77
+ } catch (error) {
78
+ if (!isMissingConfigError(error)) {
79
+ const message = error instanceof SyntaxError
80
+ ? 'Failed to parse existing personal routing config; overwriting task_routing'
81
+ : `Failed to load existing personal routing config; overwriting task_routing (${error?.message || 'unknown error'})`;
82
+ console.warn(`[TLC WARNING] ${message}`);
83
+ }
75
84
  }
76
85
 
77
86
  // Merge: replace task_routing, keep everything else