tlc-claude-code 2.2.1 → 2.4.0
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/.claude/agents/builder.md +17 -0
- package/.claude/commands/tlc/audit.md +12 -0
- package/.claude/commands/tlc/autofix.md +31 -0
- package/.claude/commands/tlc/build.md +98 -24
- package/.claude/commands/tlc/coverage.md +31 -0
- package/.claude/commands/tlc/discuss.md +31 -0
- package/.claude/commands/tlc/docs.md +31 -0
- package/.claude/commands/tlc/edge-cases.md +31 -0
- package/.claude/commands/tlc/guard.md +9 -0
- package/.claude/commands/tlc/init.md +12 -1
- package/.claude/commands/tlc/plan.md +31 -0
- package/.claude/commands/tlc/quick.md +31 -0
- package/.claude/commands/tlc/review.md +50 -0
- package/.claude/hooks/tlc-session-init.sh +14 -3
- package/CODING-STANDARDS.md +217 -10
- package/bin/setup-autoupdate.js +316 -87
- package/bin/setup-autoupdate.test.js +454 -34
- package/package.json +1 -1
- package/scripts/project-docs.js +1 -1
- package/server/lib/careful-patterns.js +142 -0
- package/server/lib/careful-patterns.test.js +164 -0
- package/server/lib/cli-dispatcher.js +98 -0
- package/server/lib/cli-dispatcher.test.js +249 -0
- package/server/lib/command-router.js +171 -0
- package/server/lib/command-router.test.js +336 -0
- package/server/lib/field-report.js +92 -0
- package/server/lib/field-report.test.js +195 -0
- package/server/lib/orchestration/worktree-manager.js +133 -0
- package/server/lib/orchestration/worktree-manager.test.js +198 -0
- package/server/lib/overdrive-command.js +31 -9
- package/server/lib/overdrive-command.test.js +25 -26
- package/server/lib/prompt-packager.js +98 -0
- package/server/lib/prompt-packager.test.js +185 -0
- package/server/lib/review-fixer.js +107 -0
- package/server/lib/review-fixer.test.js +152 -0
- package/server/lib/routing-command.js +159 -0
- package/server/lib/routing-command.test.js +290 -0
- package/server/lib/scope-checker.js +127 -0
- package/server/lib/scope-checker.test.js +175 -0
- package/server/lib/skill-validator.js +165 -0
- package/server/lib/skill-validator.test.js +289 -0
- package/server/lib/standards/standards-injector.js +6 -0
- package/server/lib/task-router-config.js +142 -0
- package/server/lib/task-router-config.test.js +428 -0
- package/server/lib/test-selector.js +127 -0
- package/server/lib/test-selector.test.js +172 -0
- package/server/setup.sh +271 -271
- package/server/templates/CLAUDE.md +6 -0
- package/server/templates/CODING-STANDARDS.md +356 -10
|
@@ -1,88 +1,430 @@
|
|
|
1
|
-
import { describe, it, expect, vi
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
2
|
import {
|
|
3
|
-
generateLaunchdPlist,
|
|
4
|
-
generateCronEntry,
|
|
5
3
|
generateUpdateScript,
|
|
4
|
+
generateCronEntry,
|
|
5
|
+
generateLaunchdPlist,
|
|
6
6
|
enable,
|
|
7
7
|
disable,
|
|
8
8
|
writeTimestamp,
|
|
9
9
|
readTimestamp,
|
|
10
10
|
isStale,
|
|
11
|
+
checkForUpdate,
|
|
12
|
+
CLI_PACKAGES,
|
|
13
|
+
TLC_PACKAGE,
|
|
11
14
|
} from './setup-autoupdate.js';
|
|
12
15
|
|
|
13
16
|
describe('setup-autoupdate', () => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
expect(
|
|
20
|
-
|
|
17
|
+
// ───────────────────────────────────────────────────
|
|
18
|
+
// CLI_PACKAGES constant
|
|
19
|
+
// ───────────────────────────────────────────────────
|
|
20
|
+
describe('CLI_PACKAGES', () => {
|
|
21
|
+
it('maps claude to its npm and homebrew package names', () => {
|
|
22
|
+
expect(CLI_PACKAGES.claude).toEqual({
|
|
23
|
+
npm: '@anthropic-ai/claude-code',
|
|
24
|
+
brew: 'claude-code',
|
|
25
|
+
selfUpdate: 'claude update',
|
|
26
|
+
});
|
|
21
27
|
});
|
|
22
|
-
});
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
it('maps codex to its npm and homebrew package names', () => {
|
|
30
|
+
expect(CLI_PACKAGES.codex).toEqual({
|
|
31
|
+
npm: '@openai/codex',
|
|
32
|
+
brew: 'codex',
|
|
33
|
+
selfUpdate: null,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('maps gemini to its npm and homebrew package names', () => {
|
|
38
|
+
expect(CLI_PACKAGES.gemini).toEqual({
|
|
39
|
+
npm: '@google/gemini-cli',
|
|
40
|
+
brew: 'gemini-cli',
|
|
41
|
+
selfUpdate: null,
|
|
42
|
+
});
|
|
29
43
|
});
|
|
30
44
|
});
|
|
31
45
|
|
|
46
|
+
// ───────────────────────────────────────────────────
|
|
47
|
+
// generateUpdateScript — self-contained bash script
|
|
48
|
+
// ───────────────────────────────────────────────────
|
|
32
49
|
describe('generateUpdateScript', () => {
|
|
33
|
-
it('
|
|
50
|
+
it('starts with bash shebang and strict mode', () => {
|
|
51
|
+
const script = generateUpdateScript();
|
|
52
|
+
expect(script).toMatch(/^#!\/usr\/bin\/env bash\n/);
|
|
53
|
+
expect(script).toContain('set -euo pipefail');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('defines a detect_install_method function', () => {
|
|
57
|
+
const script = generateUpdateScript();
|
|
58
|
+
expect(script).toContain('detect_install_method');
|
|
59
|
+
expect(script).toContain('brew list --formula');
|
|
60
|
+
expect(script).toContain('npm list -g');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('defines an update_cli function that dispatches by install method', () => {
|
|
64
|
+
const script = generateUpdateScript();
|
|
65
|
+
expect(script).toContain('update_cli');
|
|
66
|
+
expect(script).toContain('brew upgrade');
|
|
67
|
+
expect(script).toContain('npm update -g');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('includes update blocks for all three CLIs', () => {
|
|
71
|
+
const script = generateUpdateScript();
|
|
72
|
+
expect(script).toContain('claude');
|
|
73
|
+
expect(script).toContain('codex');
|
|
74
|
+
expect(script).toContain('gemini');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('handles claude self-update for standalone installs', () => {
|
|
34
78
|
const script = generateUpdateScript();
|
|
35
79
|
expect(script).toContain('claude update');
|
|
36
80
|
});
|
|
37
81
|
|
|
38
|
-
it('includes
|
|
82
|
+
it('includes homebrew detection for each CLI', () => {
|
|
39
83
|
const script = generateUpdateScript();
|
|
40
|
-
expect(script).toContain('
|
|
84
|
+
expect(script).toContain('claude-code');
|
|
85
|
+
expect(script).toContain('@openai/codex');
|
|
86
|
+
expect(script).toContain('@google/gemini-cli');
|
|
41
87
|
});
|
|
42
88
|
|
|
43
|
-
it('logs to autoupdate.log', () => {
|
|
89
|
+
it('logs to autoupdate.log with timestamps', () => {
|
|
44
90
|
const script = generateUpdateScript();
|
|
45
91
|
expect(script).toContain('autoupdate.log');
|
|
92
|
+
expect(script).toContain('date -u');
|
|
46
93
|
});
|
|
47
94
|
|
|
48
|
-
it('writes timestamp to .last-update', () => {
|
|
95
|
+
it('writes completion timestamp to .last-update', () => {
|
|
49
96
|
const script = generateUpdateScript();
|
|
50
97
|
expect(script).toContain('.last-update');
|
|
51
98
|
});
|
|
99
|
+
|
|
100
|
+
it('continues on individual CLI failure without aborting', () => {
|
|
101
|
+
const script = generateUpdateScript();
|
|
102
|
+
// Each update command should have || error handling, not abort on failure
|
|
103
|
+
const updateLines = script.split('\n').filter(l => l.includes('>> "$LOG_FILE" 2>&1'));
|
|
104
|
+
for (const line of updateLines) {
|
|
105
|
+
expect(line).toContain('||');
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('skips CLIs that are not installed', () => {
|
|
110
|
+
const script = generateUpdateScript();
|
|
111
|
+
// Each CLI block should check command -v before attempting update
|
|
112
|
+
expect(script).toContain('command -v claude');
|
|
113
|
+
expect(script).toContain('command -v codex');
|
|
114
|
+
expect(script).toContain('command -v gemini');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('loads shell profile to find brew and npm in PATH', () => {
|
|
118
|
+
const script = generateUpdateScript();
|
|
119
|
+
// Cron jobs have minimal PATH — script must source profile
|
|
120
|
+
expect(script).toMatch(/source.*profile|\.zshrc|\.bashrc|PATH.*homebrew|\/opt\/homebrew|\/usr\/local/);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('sources version managers (nvm, volta, asdf) for PATH', () => {
|
|
124
|
+
const script = generateUpdateScript();
|
|
125
|
+
expect(script).toContain('.nvm/nvm.sh');
|
|
126
|
+
expect(script).toContain('.volta/bin');
|
|
127
|
+
expect(script).toContain('.asdf/shims');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('updates TLC itself via npm', () => {
|
|
131
|
+
const script = generateUpdateScript();
|
|
132
|
+
expect(script).toContain('tlc-claude-code');
|
|
133
|
+
expect(script).toContain('npm update -g');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('checks for TLC update availability and writes notification file', () => {
|
|
137
|
+
const script = generateUpdateScript();
|
|
138
|
+
expect(script).toContain('.update-available');
|
|
139
|
+
expect(script).toContain('npm show tlc-claude-code version');
|
|
140
|
+
});
|
|
52
141
|
});
|
|
53
142
|
|
|
143
|
+
// ───────────────────────────────────────────────────
|
|
144
|
+
// generateCronEntry
|
|
145
|
+
// ───────────────────────────────────────────────────
|
|
146
|
+
describe('generateCronEntry', () => {
|
|
147
|
+
it('returns a valid crontab line running at 4am daily', () => {
|
|
148
|
+
const entry = generateCronEntry('/path/to/script.sh');
|
|
149
|
+
expect(entry).toMatch(/^0 4 \* \* \*/);
|
|
150
|
+
expect(entry).toContain('/path/to/script.sh');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('includes a comment marker for identification', () => {
|
|
154
|
+
const entry = generateCronEntry('/path/to/script.sh');
|
|
155
|
+
expect(entry).toContain('# tlc-autoupdate');
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ───────────────────────────────────────────────────
|
|
160
|
+
// enable — launchd on macOS, cron on Linux
|
|
161
|
+
// ───────────────────────────────────────────────────
|
|
54
162
|
describe('enable', () => {
|
|
55
|
-
it('
|
|
56
|
-
const fs = {
|
|
163
|
+
it('uses launchd on macOS', () => {
|
|
164
|
+
const fs = {
|
|
165
|
+
writeFileSync: vi.fn(),
|
|
166
|
+
mkdirSync: vi.fn(),
|
|
167
|
+
chmodSync: vi.fn(),
|
|
168
|
+
};
|
|
57
169
|
const result = enable({ platform: 'darwin', fs });
|
|
58
170
|
expect(result.type).toBe('launchd');
|
|
59
|
-
expect(
|
|
171
|
+
expect(result.plistPath).toContain('com.tlc.autoupdate.plist');
|
|
60
172
|
});
|
|
61
173
|
|
|
62
|
-
it('
|
|
174
|
+
it('writes valid launchd plist with daily interval on macOS', () => {
|
|
175
|
+
const fs = {
|
|
176
|
+
writeFileSync: vi.fn(),
|
|
177
|
+
mkdirSync: vi.fn(),
|
|
178
|
+
chmodSync: vi.fn(),
|
|
179
|
+
};
|
|
180
|
+
enable({ platform: 'darwin', fs });
|
|
181
|
+
|
|
182
|
+
const plistCall = fs.writeFileSync.mock.calls.find(c => c[0].endsWith('.plist'));
|
|
183
|
+
expect(plistCall).toBeDefined();
|
|
184
|
+
expect(plistCall[1]).toContain('<?xml');
|
|
185
|
+
expect(plistCall[1]).toContain('<integer>86400</integer>');
|
|
186
|
+
expect(plistCall[1]).toContain('autoupdate.sh');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('uses cron on Linux', () => {
|
|
63
190
|
const execSync = vi.fn(() => '');
|
|
64
|
-
const fs = {
|
|
191
|
+
const fs = {
|
|
192
|
+
writeFileSync: vi.fn(),
|
|
193
|
+
mkdirSync: vi.fn(),
|
|
194
|
+
chmodSync: vi.fn(),
|
|
195
|
+
};
|
|
65
196
|
const result = enable({ platform: 'linux', fs, execSync });
|
|
66
197
|
expect(result.type).toBe('cron');
|
|
67
198
|
});
|
|
199
|
+
|
|
200
|
+
it('writes the update script and makes it executable', () => {
|
|
201
|
+
const fs = {
|
|
202
|
+
writeFileSync: vi.fn(),
|
|
203
|
+
mkdirSync: vi.fn(),
|
|
204
|
+
chmodSync: vi.fn(),
|
|
205
|
+
};
|
|
206
|
+
enable({ platform: 'darwin', fs });
|
|
207
|
+
|
|
208
|
+
const scriptCall = fs.writeFileSync.mock.calls.find(c => c[0].endsWith('autoupdate.sh'));
|
|
209
|
+
expect(scriptCall).toBeDefined();
|
|
210
|
+
expect(scriptCall[1]).toContain('#!/usr/bin/env bash');
|
|
211
|
+
|
|
212
|
+
expect(fs.chmodSync).toHaveBeenCalledWith(
|
|
213
|
+
expect.stringContaining('autoupdate.sh'),
|
|
214
|
+
0o755
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('creates TLC directories', () => {
|
|
219
|
+
const fs = {
|
|
220
|
+
writeFileSync: vi.fn(),
|
|
221
|
+
mkdirSync: vi.fn(),
|
|
222
|
+
chmodSync: vi.fn(),
|
|
223
|
+
};
|
|
224
|
+
enable({ platform: 'darwin', fs });
|
|
225
|
+
|
|
226
|
+
const dirs = fs.mkdirSync.mock.calls.map(c => c[0]);
|
|
227
|
+
expect(dirs.some(d => d.endsWith('.tlc'))).toBe(true);
|
|
228
|
+
expect(dirs.some(d => d.includes('logs'))).toBe(true);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('appends cron entry to existing crontab on Linux', () => {
|
|
232
|
+
const existingCrontab = '30 2 * * * /usr/bin/backup\n';
|
|
233
|
+
const execSync = vi.fn((cmd) => {
|
|
234
|
+
if (cmd.startsWith('crontab -l')) return existingCrontab;
|
|
235
|
+
return '';
|
|
236
|
+
});
|
|
237
|
+
const fs = {
|
|
238
|
+
writeFileSync: vi.fn(),
|
|
239
|
+
mkdirSync: vi.fn(),
|
|
240
|
+
chmodSync: vi.fn(),
|
|
241
|
+
};
|
|
242
|
+
enable({ platform: 'linux', fs, execSync });
|
|
243
|
+
|
|
244
|
+
// crontab content is piped via { input } option, not in the command string
|
|
245
|
+
const installCall = execSync.mock.calls.find(c =>
|
|
246
|
+
typeof c[0] === 'string' && c[0] === 'crontab -'
|
|
247
|
+
);
|
|
248
|
+
expect(installCall).toBeDefined();
|
|
249
|
+
expect(installCall[1].input).toContain('backup');
|
|
250
|
+
expect(installCall[1].input).toContain('tlc-autoupdate');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('does not duplicate if cron entry already exists', () => {
|
|
254
|
+
const existingCrontab = '0 4 * * * /home/user/.tlc/autoupdate.sh # tlc-autoupdate\n';
|
|
255
|
+
const execSync = vi.fn((cmd) => {
|
|
256
|
+
if (cmd.startsWith('crontab -l')) return existingCrontab;
|
|
257
|
+
return '';
|
|
258
|
+
});
|
|
259
|
+
const fs = {
|
|
260
|
+
writeFileSync: vi.fn(),
|
|
261
|
+
mkdirSync: vi.fn(),
|
|
262
|
+
chmodSync: vi.fn(),
|
|
263
|
+
};
|
|
264
|
+
const result = enable({ platform: 'linux', fs, execSync });
|
|
265
|
+
|
|
266
|
+
expect(result.type).toBe('cron');
|
|
267
|
+
const installCalls = execSync.mock.calls.filter(c =>
|
|
268
|
+
typeof c[0] === 'string' && c[0].includes('crontab') && !c[0].includes('-l')
|
|
269
|
+
);
|
|
270
|
+
expect(installCalls).toHaveLength(0);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('handles empty crontab gracefully on Linux', () => {
|
|
274
|
+
const execSync = vi.fn((cmd) => {
|
|
275
|
+
if (cmd.startsWith('crontab -l')) throw new Error('no crontab for user');
|
|
276
|
+
return '';
|
|
277
|
+
});
|
|
278
|
+
const fs = {
|
|
279
|
+
writeFileSync: vi.fn(),
|
|
280
|
+
mkdirSync: vi.fn(),
|
|
281
|
+
chmodSync: vi.fn(),
|
|
282
|
+
};
|
|
283
|
+
const result = enable({ platform: 'linux', fs, execSync });
|
|
284
|
+
expect(result.type).toBe('cron');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('returns the script path', () => {
|
|
288
|
+
const fs = {
|
|
289
|
+
writeFileSync: vi.fn(),
|
|
290
|
+
mkdirSync: vi.fn(),
|
|
291
|
+
chmodSync: vi.fn(),
|
|
292
|
+
};
|
|
293
|
+
const result = enable({ platform: 'darwin', fs });
|
|
294
|
+
expect(result.scriptPath).toContain('autoupdate.sh');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('replaces legacy unmarked cron entry with marked one on Linux', () => {
|
|
298
|
+
const home = process.env.HOME || '/home/user';
|
|
299
|
+
const legacyCrontab = `30 2 * * * /usr/bin/backup\n0 4 * * * ${home}/.tlc/autoupdate.sh\n`;
|
|
300
|
+
const execSync = vi.fn((cmd) => {
|
|
301
|
+
if (cmd.startsWith('crontab -l')) return legacyCrontab;
|
|
302
|
+
return '';
|
|
303
|
+
});
|
|
304
|
+
const fs = {
|
|
305
|
+
writeFileSync: vi.fn(),
|
|
306
|
+
mkdirSync: vi.fn(),
|
|
307
|
+
chmodSync: vi.fn(),
|
|
308
|
+
};
|
|
309
|
+
enable({ platform: 'linux', fs, execSync });
|
|
310
|
+
|
|
311
|
+
const installCall = execSync.mock.calls.find(c =>
|
|
312
|
+
typeof c[0] === 'string' && c[0] === 'crontab -'
|
|
313
|
+
);
|
|
314
|
+
expect(installCall).toBeDefined();
|
|
315
|
+
const input = installCall[1].input;
|
|
316
|
+
expect(input).toContain('tlc-autoupdate');
|
|
317
|
+
expect(input).toContain('backup');
|
|
318
|
+
const lines = input.split('\n').filter(l => l.includes('autoupdate.sh'));
|
|
319
|
+
expect(lines).toHaveLength(1);
|
|
320
|
+
});
|
|
68
321
|
});
|
|
69
322
|
|
|
323
|
+
// ───────────────────────────────────────────────────
|
|
324
|
+
// disable — launchd on macOS, cron on Linux
|
|
325
|
+
// ───────────────────────────────────────────────────
|
|
70
326
|
describe('disable', () => {
|
|
71
327
|
it('removes launchd plist on macOS', () => {
|
|
72
|
-
const fs = {
|
|
328
|
+
const fs = {
|
|
329
|
+
existsSync: vi.fn(() => true),
|
|
330
|
+
unlinkSync: vi.fn(),
|
|
331
|
+
};
|
|
73
332
|
const result = disable({ platform: 'darwin', fs });
|
|
74
333
|
expect(result.removed).toBe(true);
|
|
75
|
-
expect(fs.unlinkSync).
|
|
334
|
+
expect(fs.unlinkSync).toHaveBeenCalledWith(
|
|
335
|
+
expect.stringContaining('com.tlc.autoupdate.plist')
|
|
336
|
+
);
|
|
76
337
|
});
|
|
77
338
|
|
|
78
|
-
it('
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
339
|
+
it('returns removed: false on macOS when no plist exists', () => {
|
|
340
|
+
const fs = {
|
|
341
|
+
existsSync: vi.fn(() => false),
|
|
342
|
+
};
|
|
343
|
+
const result = disable({ platform: 'darwin', fs });
|
|
344
|
+
expect(result.removed).toBe(false);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('removes the tlc-autoupdate cron entry on Linux', () => {
|
|
348
|
+
const existingCrontab = '30 2 * * * /usr/bin/backup\n0 4 * * * /home/user/.tlc/autoupdate.sh # tlc-autoupdate\n';
|
|
349
|
+
const execSync = vi.fn((cmd) => {
|
|
350
|
+
if (cmd.startsWith('crontab -l')) return existingCrontab;
|
|
351
|
+
return '';
|
|
352
|
+
});
|
|
353
|
+
disable({ platform: 'linux', execSync });
|
|
354
|
+
|
|
355
|
+
const installCall = execSync.mock.calls.find(c =>
|
|
356
|
+
typeof c[0] === 'string' && c[0] === 'crontab -'
|
|
357
|
+
);
|
|
358
|
+
expect(installCall).toBeDefined();
|
|
359
|
+
expect(installCall[1].input).toContain('backup');
|
|
360
|
+
expect(installCall[1].input).not.toContain('tlc-autoupdate');
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('returns removed: true when cron entry was found on Linux', () => {
|
|
364
|
+
const existingCrontab = '0 4 * * * /home/user/.tlc/autoupdate.sh # tlc-autoupdate\n';
|
|
365
|
+
const execSync = vi.fn((cmd) => {
|
|
366
|
+
if (cmd.startsWith('crontab -l')) return existingCrontab;
|
|
367
|
+
return '';
|
|
368
|
+
});
|
|
369
|
+
const result = disable({ platform: 'linux', execSync });
|
|
82
370
|
expect(result.removed).toBe(true);
|
|
83
371
|
});
|
|
372
|
+
|
|
373
|
+
it('returns removed: false when no cron entry exists on Linux', () => {
|
|
374
|
+
const execSync = vi.fn((cmd) => {
|
|
375
|
+
if (cmd.startsWith('crontab -l')) return '30 2 * * * /usr/bin/backup\n';
|
|
376
|
+
return '';
|
|
377
|
+
});
|
|
378
|
+
const result = disable({ platform: 'linux', execSync });
|
|
379
|
+
expect(result.removed).toBe(false);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('preserves other cron entries on Linux', () => {
|
|
383
|
+
const existingCrontab = '30 2 * * * /usr/bin/backup\n0 4 * * * /home/user/.tlc/autoupdate.sh # tlc-autoupdate\n15 3 * * 1 /usr/bin/weekly\n';
|
|
384
|
+
const execSync = vi.fn((cmd) => {
|
|
385
|
+
if (cmd.startsWith('crontab -l')) return existingCrontab;
|
|
386
|
+
return '';
|
|
387
|
+
});
|
|
388
|
+
disable({ platform: 'linux', execSync });
|
|
389
|
+
|
|
390
|
+
const installCall = execSync.mock.calls.find(c =>
|
|
391
|
+
typeof c[0] === 'string' && c[0] === 'crontab -'
|
|
392
|
+
);
|
|
393
|
+
expect(installCall[1].input).toContain('backup');
|
|
394
|
+
expect(installCall[1].input).toContain('weekly');
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('handles empty crontab gracefully on Linux', () => {
|
|
398
|
+
const execSync = vi.fn((cmd) => {
|
|
399
|
+
if (cmd.startsWith('crontab -l')) throw new Error('no crontab for user');
|
|
400
|
+
return '';
|
|
401
|
+
});
|
|
402
|
+
const result = disable({ platform: 'linux', execSync });
|
|
403
|
+
expect(result.removed).toBe(false);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('removes legacy unmarked cron entries from old versions', () => {
|
|
407
|
+
const home = process.env.HOME || '/home/user';
|
|
408
|
+
const legacyCrontab = `30 2 * * * /usr/bin/backup\n0 4 * * * ${home}/.tlc/autoupdate.sh\n`;
|
|
409
|
+
const execSync = vi.fn((cmd) => {
|
|
410
|
+
if (cmd.startsWith('crontab -l')) return legacyCrontab;
|
|
411
|
+
return '';
|
|
412
|
+
});
|
|
413
|
+
const result = disable({ platform: 'linux', execSync });
|
|
414
|
+
expect(result.removed).toBe(true);
|
|
415
|
+
|
|
416
|
+
const installCall = execSync.mock.calls.find(c =>
|
|
417
|
+
typeof c[0] === 'string' && c[0] === 'crontab -'
|
|
418
|
+
);
|
|
419
|
+
expect(installCall).toBeDefined();
|
|
420
|
+
expect(installCall[1].input).toContain('backup');
|
|
421
|
+
expect(installCall[1].input).not.toContain('autoupdate.sh');
|
|
422
|
+
});
|
|
84
423
|
});
|
|
85
424
|
|
|
425
|
+
// ───────────────────────────────────────────────────
|
|
426
|
+
// writeTimestamp / readTimestamp
|
|
427
|
+
// ───────────────────────────────────────────────────
|
|
86
428
|
describe('writeTimestamp / readTimestamp', () => {
|
|
87
429
|
it('writes ISO timestamp to .last-update', () => {
|
|
88
430
|
const fs = { writeFileSync: vi.fn(), mkdirSync: vi.fn() };
|
|
@@ -106,14 +448,17 @@ describe('setup-autoupdate', () => {
|
|
|
106
448
|
});
|
|
107
449
|
});
|
|
108
450
|
|
|
451
|
+
// ───────────────────────────────────────────────────
|
|
452
|
+
// isStale
|
|
453
|
+
// ───────────────────────────────────────────────────
|
|
109
454
|
describe('isStale', () => {
|
|
110
455
|
it('returns false for fresh timestamp (<24h)', () => {
|
|
111
|
-
const recent = new Date(Date.now() - 3600_000).toISOString();
|
|
456
|
+
const recent = new Date(Date.now() - 3600_000).toISOString();
|
|
112
457
|
expect(isStale(recent)).toBe(false);
|
|
113
458
|
});
|
|
114
459
|
|
|
115
460
|
it('returns true for stale timestamp (>24h)', () => {
|
|
116
|
-
const old = new Date(Date.now() - 90_000_000).toISOString();
|
|
461
|
+
const old = new Date(Date.now() - 90_000_000).toISOString();
|
|
117
462
|
expect(isStale(old)).toBe(true);
|
|
118
463
|
});
|
|
119
464
|
|
|
@@ -121,4 +466,79 @@ describe('setup-autoupdate', () => {
|
|
|
121
466
|
expect(isStale(null)).toBe(true);
|
|
122
467
|
});
|
|
123
468
|
});
|
|
469
|
+
|
|
470
|
+
// ───────────────────────────────────────────────────
|
|
471
|
+
// TLC_PACKAGE constant
|
|
472
|
+
// ───────────────────────────────────────────────────
|
|
473
|
+
describe('TLC_PACKAGE', () => {
|
|
474
|
+
it('has the correct npm package name', () => {
|
|
475
|
+
expect(TLC_PACKAGE).toBe('tlc-claude-code');
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// ───────────────────────────────────────────────────
|
|
480
|
+
// checkForUpdate — version check + notification
|
|
481
|
+
// ───────────────────────────────────────────────────
|
|
482
|
+
describe('checkForUpdate', () => {
|
|
483
|
+
it('returns null when installed version matches latest', () => {
|
|
484
|
+
const execSync = vi.fn(() => '2.3.0');
|
|
485
|
+
const result = checkForUpdate({ execSync, installedVersion: '2.3.0' });
|
|
486
|
+
expect(result).toBeNull();
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('returns update info when a newer version is available', () => {
|
|
490
|
+
const execSync = vi.fn(() => '2.4.0\n');
|
|
491
|
+
const result = checkForUpdate({ execSync, installedVersion: '2.3.0' });
|
|
492
|
+
expect(result).toEqual({
|
|
493
|
+
current: '2.3.0',
|
|
494
|
+
latest: '2.4.0',
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('returns null when npm show fails', () => {
|
|
499
|
+
const execSync = vi.fn(() => { throw new Error('network error'); });
|
|
500
|
+
const result = checkForUpdate({ execSync, installedVersion: '2.3.0' });
|
|
501
|
+
expect(result).toBeNull();
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('returns null when latest is older than installed (pre-release)', () => {
|
|
505
|
+
const execSync = vi.fn(() => '2.2.0');
|
|
506
|
+
const result = checkForUpdate({ execSync, installedVersion: '2.3.0' });
|
|
507
|
+
expect(result).toBeNull();
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('writes notification file when update is available', () => {
|
|
511
|
+
const execSync = vi.fn(() => '2.5.0');
|
|
512
|
+
const fs = { writeFileSync: vi.fn(), mkdirSync: vi.fn() };
|
|
513
|
+
checkForUpdate({ execSync, installedVersion: '2.3.0', fs, dir: '/tmp/.tlc' });
|
|
514
|
+
|
|
515
|
+
const writeCall = fs.writeFileSync.mock.calls.find(c => c[0].includes('.update-available'));
|
|
516
|
+
expect(writeCall).toBeDefined();
|
|
517
|
+
expect(writeCall[1]).toContain('2.5.0');
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('removes notification file when up to date', () => {
|
|
521
|
+
const execSync = vi.fn(() => '2.3.0');
|
|
522
|
+
const fs = {
|
|
523
|
+
existsSync: vi.fn(() => true),
|
|
524
|
+
unlinkSync: vi.fn(),
|
|
525
|
+
};
|
|
526
|
+
checkForUpdate({ execSync, installedVersion: '2.3.0', fs, dir: '/tmp/.tlc' });
|
|
527
|
+
|
|
528
|
+
expect(fs.unlinkSync).toHaveBeenCalledWith(
|
|
529
|
+
expect.stringContaining('.update-available')
|
|
530
|
+
);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('formats notification message with versions and command', () => {
|
|
534
|
+
const execSync = vi.fn(() => '3.0.0');
|
|
535
|
+
const fs = { writeFileSync: vi.fn(), mkdirSync: vi.fn() };
|
|
536
|
+
checkForUpdate({ execSync, installedVersion: '2.3.0', fs, dir: '/tmp/.tlc' });
|
|
537
|
+
|
|
538
|
+
const content = fs.writeFileSync.mock.calls.find(c => c[0].includes('.update-available'))[1];
|
|
539
|
+
expect(content).toContain('2.3.0');
|
|
540
|
+
expect(content).toContain('3.0.0');
|
|
541
|
+
expect(content).toContain('npm update -g tlc-claude-code');
|
|
542
|
+
});
|
|
543
|
+
});
|
|
124
544
|
});
|
package/package.json
CHANGED
package/scripts/project-docs.js
CHANGED