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
package/bin/setup-autoupdate.js
CHANGED
|
@@ -1,30 +1,230 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TLC Auto-Update Setup
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Generates a self-contained bash script that keeps Claude, Codex, and Gemini
|
|
5
|
+
* CLIs updated daily. The script detects each CLI's install method (Homebrew,
|
|
6
|
+
* npm global, standalone binary) at runtime and uses the correct update command.
|
|
7
|
+
* macOS: scheduled via launchd (runs missed jobs after wake).
|
|
8
|
+
* Linux: scheduled via cron.
|
|
9
|
+
* No Node.js dependency at runtime — pure bash.
|
|
4
10
|
*/
|
|
5
11
|
|
|
6
12
|
import os from 'os';
|
|
7
13
|
import path from 'path';
|
|
8
14
|
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const SCRIPT_PATH = `${TLC_DIR}/autoupdate.sh`;
|
|
13
|
-
const LOG_PATH = `${TLC_DIR}/logs/autoupdate.log`;
|
|
15
|
+
const TLC_DIR = path.join(os.homedir(), '.tlc');
|
|
16
|
+
const SCRIPT_PATH = path.join(TLC_DIR, 'autoupdate.sh');
|
|
17
|
+
const CRON_MARKER = '# tlc-autoupdate';
|
|
14
18
|
const TIMESTAMP_FILE = '.last-update';
|
|
19
|
+
const LAUNCHD_PLIST = path.join(os.homedir(), 'Library', 'LaunchAgents', 'com.tlc.autoupdate.plist');
|
|
20
|
+
const UPDATE_AVAILABLE_FILE = '.update-available';
|
|
21
|
+
|
|
22
|
+
/** @type {string} npm package name for TLC */
|
|
23
|
+
export const TLC_PACKAGE = 'tlc-claude-code';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Package names per install method for each supported CLI.
|
|
27
|
+
* @type {Record<string, { npm: string, brew: string, selfUpdate: string|null }>}
|
|
28
|
+
*/
|
|
29
|
+
export const CLI_PACKAGES = {
|
|
30
|
+
claude: {
|
|
31
|
+
npm: '@anthropic-ai/claude-code',
|
|
32
|
+
brew: 'claude-code',
|
|
33
|
+
selfUpdate: 'claude update',
|
|
34
|
+
},
|
|
35
|
+
codex: {
|
|
36
|
+
npm: '@openai/codex',
|
|
37
|
+
brew: 'codex',
|
|
38
|
+
selfUpdate: null,
|
|
39
|
+
},
|
|
40
|
+
gemini: {
|
|
41
|
+
npm: '@google/gemini-cli',
|
|
42
|
+
brew: 'gemini-cli',
|
|
43
|
+
selfUpdate: null,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Generates a self-contained bash script that detects install methods at
|
|
49
|
+
* runtime and updates all three CLIs. Designed to run from cron with a
|
|
50
|
+
* minimal environment.
|
|
51
|
+
* @returns {string} Complete bash script content.
|
|
52
|
+
*/
|
|
53
|
+
export function generateUpdateScript() {
|
|
54
|
+
return `#!/usr/bin/env bash
|
|
55
|
+
set -euo pipefail
|
|
56
|
+
|
|
57
|
+
# ── PATH setup (cron has minimal PATH) ──────────────────────────────
|
|
58
|
+
# Homebrew paths
|
|
59
|
+
if [ -d "/opt/homebrew/bin" ]; then
|
|
60
|
+
export PATH="/opt/homebrew/bin:\$PATH"
|
|
61
|
+
fi
|
|
62
|
+
if [ -d "/usr/local/bin" ]; then
|
|
63
|
+
export PATH="/usr/local/bin:\$PATH"
|
|
64
|
+
fi
|
|
65
|
+
# Version managers (nvm, volta, asdf)
|
|
66
|
+
[ -s "\$HOME/.nvm/nvm.sh" ] && . "\$HOME/.nvm/nvm.sh" 2>/dev/null
|
|
67
|
+
[ -d "\$HOME/.volta/bin" ] && export PATH="\$HOME/.volta/bin:\$PATH"
|
|
68
|
+
[ -d "\$HOME/.asdf/shims" ] && export PATH="\$HOME/.asdf/shims:\$PATH"
|
|
69
|
+
# npm global bin (fallback for non-managed installs)
|
|
70
|
+
NPM_BIN="\$(npm config get prefix 2>/dev/null)/bin" || true
|
|
71
|
+
if [ -d "\$NPM_BIN" ]; then
|
|
72
|
+
export PATH="\$NPM_BIN:\$PATH"
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# ── Directories ─────────────────────────────────────────────────────
|
|
76
|
+
TLC_DIR="\$HOME/.tlc"
|
|
77
|
+
LOG_DIR="\$TLC_DIR/logs"
|
|
78
|
+
LOG_FILE="\$LOG_DIR/autoupdate.log"
|
|
79
|
+
TIMESTAMP_FILE="\$TLC_DIR/.last-update"
|
|
80
|
+
|
|
81
|
+
mkdir -p "\$LOG_DIR"
|
|
82
|
+
|
|
83
|
+
log() {
|
|
84
|
+
echo "\$(date -u +%Y-%m-%dT%H:%M:%SZ) \$1" >> "\$LOG_FILE"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# ── Install method detection ────────────────────────────────────────
|
|
88
|
+
# Returns: "brew", "npm", "standalone", or "" (not installed)
|
|
89
|
+
detect_install_method() {
|
|
90
|
+
local cli_name="\$1"
|
|
91
|
+
local brew_pkg="\$2"
|
|
92
|
+
local npm_pkg="\$3"
|
|
93
|
+
|
|
94
|
+
if ! command -v "\$cli_name" &>/dev/null; then
|
|
95
|
+
echo ""
|
|
96
|
+
return
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# Check Homebrew first
|
|
100
|
+
if command -v brew &>/dev/null && brew list --formula 2>/dev/null | grep -q "^\${brew_pkg}\$"; then
|
|
101
|
+
echo "brew"
|
|
102
|
+
return
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# Check npm global
|
|
106
|
+
if command -v npm &>/dev/null && npm list -g "\$npm_pkg" --depth=0 2>/dev/null | grep -q "\$npm_pkg"; then
|
|
107
|
+
echo "npm"
|
|
108
|
+
return
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
echo "standalone"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# ── Update dispatcher ───────────────────────────────────────────────
|
|
115
|
+
update_cli() {
|
|
116
|
+
local cli_name="\$1"
|
|
117
|
+
local method="\$2"
|
|
118
|
+
local brew_pkg="\$3"
|
|
119
|
+
local npm_pkg="\$4"
|
|
120
|
+
local self_update="\${5:-}"
|
|
121
|
+
|
|
122
|
+
case "\$method" in
|
|
123
|
+
brew)
|
|
124
|
+
log "Updating \$cli_name via brew upgrade \$brew_pkg"
|
|
125
|
+
brew upgrade "\$brew_pkg" >> "\$LOG_FILE" 2>&1 || log "\$cli_name brew upgrade failed"
|
|
126
|
+
;;
|
|
127
|
+
npm)
|
|
128
|
+
log "Updating \$cli_name via npm update -g \$npm_pkg"
|
|
129
|
+
npm update -g "\$npm_pkg" >> "\$LOG_FILE" 2>&1 || log "\$cli_name npm update failed"
|
|
130
|
+
;;
|
|
131
|
+
standalone)
|
|
132
|
+
if [ -n "\$self_update" ]; then
|
|
133
|
+
log "Updating \$cli_name via \$self_update"
|
|
134
|
+
\$self_update >> "\$LOG_FILE" 2>&1 || log "\$cli_name self-update failed"
|
|
135
|
+
else
|
|
136
|
+
log "Skipping \$cli_name — standalone install with no self-update command"
|
|
137
|
+
fi
|
|
138
|
+
;;
|
|
139
|
+
*)
|
|
140
|
+
log "Skipping \$cli_name — not installed"
|
|
141
|
+
;;
|
|
142
|
+
esac
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# ── Main ────────────────────────────────────────────────────────────
|
|
146
|
+
log "Starting TLC auto-update"
|
|
147
|
+
|
|
148
|
+
# Claude Code
|
|
149
|
+
if command -v claude &>/dev/null; then
|
|
150
|
+
CLAUDE_METHOD=\$(detect_install_method "claude" "claude-code" "@anthropic-ai/claude-code")
|
|
151
|
+
update_cli "claude" "\$CLAUDE_METHOD" "claude-code" "@anthropic-ai/claude-code" "claude update"
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
# Codex CLI
|
|
155
|
+
if command -v codex &>/dev/null; then
|
|
156
|
+
CODEX_METHOD=\$(detect_install_method "codex" "codex" "@openai/codex")
|
|
157
|
+
update_cli "codex" "\$CODEX_METHOD" "codex" "@openai/codex"
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
# Gemini CLI
|
|
161
|
+
if command -v gemini &>/dev/null; then
|
|
162
|
+
GEMINI_METHOD=\$(detect_install_method "gemini" "gemini-cli" "@google/gemini-cli")
|
|
163
|
+
update_cli "gemini" "\$GEMINI_METHOD" "gemini-cli" "@google/gemini-cli"
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
# TLC itself
|
|
167
|
+
if command -v npm &>/dev/null; then
|
|
168
|
+
log "Updating TLC (tlc-claude-code) via npm"
|
|
169
|
+
npm update -g tlc-claude-code >> "\$LOG_FILE" 2>&1 || log "TLC npm update failed"
|
|
170
|
+
|
|
171
|
+
# Re-read installed version AFTER update to get current state
|
|
172
|
+
INSTALLED=\$(npm list -g tlc-claude-code --depth=0 --json 2>/dev/null | grep -o '"version": *"[^"]*"' | head -1 | grep -o '[0-9][^"]*') || true
|
|
173
|
+
LATEST=\$(npm show tlc-claude-code version 2>/dev/null) || true
|
|
174
|
+
UPDATE_FILE="\$TLC_DIR/.update-available"
|
|
175
|
+
if [ -n "\$LATEST" ] && [ -n "\$INSTALLED" ] && [ "\$LATEST" != "\$INSTALLED" ]; then
|
|
176
|
+
# POSIX-compatible version compare (no sort -V which is GNU-only)
|
|
177
|
+
ver_gt() {
|
|
178
|
+
local IFS=.
|
|
179
|
+
local i a=(\$1) b=(\$2)
|
|
180
|
+
for ((i=0; i<\${#a[@]}; i++)); do
|
|
181
|
+
[ "\${a[i]:-0}" -gt "\${b[i]:-0}" ] && return 0
|
|
182
|
+
[ "\${a[i]:-0}" -lt "\${b[i]:-0}" ] && return 1
|
|
183
|
+
done
|
|
184
|
+
return 1
|
|
185
|
+
}
|
|
186
|
+
if ver_gt "\$LATEST" "\$INSTALLED"; then
|
|
187
|
+
printf 'current=%s\\nlatest=%s\\ncommand=npm update -g tlc-claude-code\\n' "\$INSTALLED" "\$LATEST" > "\$UPDATE_FILE"
|
|
188
|
+
log "TLC update available: \$INSTALLED → \$LATEST"
|
|
189
|
+
else
|
|
190
|
+
rm -f "\$UPDATE_FILE"
|
|
191
|
+
fi
|
|
192
|
+
else
|
|
193
|
+
rm -f "\$UPDATE_FILE"
|
|
194
|
+
fi
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
# Write completion timestamp
|
|
198
|
+
date -u +%Y-%m-%dT%H:%M:%SZ > "\$TIMESTAMP_FILE"
|
|
199
|
+
|
|
200
|
+
log "TLC auto-update complete"
|
|
201
|
+
`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Generates a crontab entry string for daily auto-update at 4am.
|
|
206
|
+
* Includes a marker comment so we can identify and remove it later.
|
|
207
|
+
* @param {string} scriptPath - Absolute path to the update shell script.
|
|
208
|
+
* @returns {string} A single crontab line with trailing marker.
|
|
209
|
+
*/
|
|
210
|
+
export function generateCronEntry(scriptPath) {
|
|
211
|
+
return `0 4 * * * ${scriptPath} ${CRON_MARKER}`;
|
|
212
|
+
}
|
|
15
213
|
|
|
16
214
|
/**
|
|
17
|
-
* Generates a macOS LaunchAgent plist
|
|
215
|
+
* Generates a macOS LaunchAgent plist for daily auto-update.
|
|
216
|
+
* launchd runs missed jobs after wake, unlike cron which skips them.
|
|
18
217
|
* @param {string} scriptPath - Absolute path to the update shell script.
|
|
19
218
|
* @returns {string} XML plist content.
|
|
20
219
|
*/
|
|
21
220
|
export function generateLaunchdPlist(scriptPath) {
|
|
221
|
+
const logPath = path.join(TLC_DIR, 'logs', 'autoupdate.log');
|
|
22
222
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
23
223
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
24
224
|
<plist version="1.0">
|
|
25
225
|
<dict>
|
|
26
226
|
<key>Label</key>
|
|
27
|
-
<string
|
|
227
|
+
<string>com.tlc.autoupdate</string>
|
|
28
228
|
<key>ProgramArguments</key>
|
|
29
229
|
<array>
|
|
30
230
|
<string>/bin/bash</string>
|
|
@@ -35,136 +235,111 @@ export function generateLaunchdPlist(scriptPath) {
|
|
|
35
235
|
<key>RunAtLoad</key>
|
|
36
236
|
<false/>
|
|
37
237
|
<key>StandardOutPath</key>
|
|
38
|
-
<string>${
|
|
238
|
+
<string>${logPath}</string>
|
|
39
239
|
<key>StandardErrorPath</key>
|
|
40
|
-
<string>${
|
|
240
|
+
<string>${logPath}</string>
|
|
41
241
|
</dict>
|
|
42
242
|
</plist>
|
|
43
243
|
`;
|
|
44
244
|
}
|
|
45
245
|
|
|
46
246
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*/
|
|
51
|
-
export function generateCronEntry(scriptPath) {
|
|
52
|
-
return `0 4 * * * ${scriptPath}`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Generates a bash update script that runs `claude update` and
|
|
57
|
-
* `npm update -g @openai/codex`, logs output, and writes a timestamp.
|
|
58
|
-
* @returns {string} Bash script content.
|
|
59
|
-
*/
|
|
60
|
-
export function generateUpdateScript() {
|
|
61
|
-
return `#!/usr/bin/env bash
|
|
62
|
-
set -euo pipefail
|
|
63
|
-
|
|
64
|
-
TLC_DIR="$HOME/.tlc"
|
|
65
|
-
LOG_DIR="$TLC_DIR/logs"
|
|
66
|
-
LOG_FILE="$LOG_DIR/autoupdate.log"
|
|
67
|
-
TIMESTAMP_FILE="$TLC_DIR/.last-update"
|
|
68
|
-
|
|
69
|
-
mkdir -p "$LOG_DIR"
|
|
70
|
-
|
|
71
|
-
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) Starting TLC auto-update" >> "$LOG_FILE"
|
|
72
|
-
|
|
73
|
-
# Update Claude Code
|
|
74
|
-
if command -v claude &>/dev/null; then
|
|
75
|
-
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) Running: claude update" >> "$LOG_FILE"
|
|
76
|
-
claude update >> "$LOG_FILE" 2>&1 || echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) claude update failed" >> "$LOG_FILE"
|
|
77
|
-
fi
|
|
78
|
-
|
|
79
|
-
# Update Codex CLI
|
|
80
|
-
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) Running: npm update -g @openai/codex" >> "$LOG_FILE"
|
|
81
|
-
npm update -g @openai/codex >> "$LOG_FILE" 2>&1 || echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) npm update failed" >> "$LOG_FILE"
|
|
82
|
-
|
|
83
|
-
# Write timestamp
|
|
84
|
-
date -u +%Y-%m-%dT%H:%M:%SZ > "$TIMESTAMP_FILE"
|
|
85
|
-
|
|
86
|
-
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) TLC auto-update complete" >> "$LOG_FILE"
|
|
87
|
-
`;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Enables auto-update scheduled job for the current platform.
|
|
247
|
+
* Writes the update script and installs the platform scheduler.
|
|
248
|
+
* macOS: launchd (runs missed jobs after wake)
|
|
249
|
+
* Linux: cron
|
|
92
250
|
* @param {object} opts
|
|
93
251
|
* @param {string} opts.platform - 'darwin' or 'linux'
|
|
94
252
|
* @param {object} opts.fs - fs module (injected for testability)
|
|
95
|
-
* @param {Function} [opts.execSync] - child_process.execSync (
|
|
96
|
-
* @returns {{ type:
|
|
253
|
+
* @param {Function} [opts.execSync] - child_process.execSync (required for Linux)
|
|
254
|
+
* @returns {{ type: 'launchd'|'cron', scriptPath: string, plistPath?: string }}
|
|
97
255
|
*/
|
|
98
256
|
export function enable({ platform, fs, execSync }) {
|
|
99
|
-
// Ensure TLC directories exist
|
|
100
257
|
fs.mkdirSync(TLC_DIR, { recursive: true });
|
|
101
|
-
fs.mkdirSync(
|
|
258
|
+
fs.mkdirSync(path.join(TLC_DIR, 'logs'), { recursive: true });
|
|
102
259
|
|
|
103
|
-
// Write the update script
|
|
104
260
|
const scriptContent = generateUpdateScript();
|
|
105
261
|
fs.writeFileSync(SCRIPT_PATH, scriptContent, 'utf8');
|
|
106
262
|
fs.chmodSync(SCRIPT_PATH, 0o755);
|
|
107
263
|
|
|
108
264
|
if (platform === 'darwin') {
|
|
109
|
-
const
|
|
110
|
-
fs.mkdirSync(
|
|
265
|
+
const plistDir = path.dirname(LAUNCHD_PLIST);
|
|
266
|
+
fs.mkdirSync(plistDir, { recursive: true });
|
|
111
267
|
const plist = generateLaunchdPlist(SCRIPT_PATH);
|
|
112
|
-
fs.writeFileSync(
|
|
113
|
-
return { type: 'launchd', scriptPath: SCRIPT_PATH,
|
|
268
|
+
fs.writeFileSync(LAUNCHD_PLIST, plist, 'utf8');
|
|
269
|
+
return { type: 'launchd', scriptPath: SCRIPT_PATH, plistPath: LAUNCHD_PLIST };
|
|
114
270
|
}
|
|
115
271
|
|
|
116
|
-
// Linux
|
|
117
|
-
const entry = generateCronEntry(SCRIPT_PATH);
|
|
272
|
+
// Linux: cron
|
|
118
273
|
let existing = '';
|
|
119
274
|
try {
|
|
120
275
|
existing = execSync('crontab -l 2>/dev/null || true', { encoding: 'utf8' });
|
|
121
|
-
} catch
|
|
276
|
+
} catch {
|
|
122
277
|
existing = '';
|
|
123
278
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
279
|
+
|
|
280
|
+
if (existing.includes(CRON_MARKER)) {
|
|
281
|
+
return { type: 'cron', scriptPath: SCRIPT_PATH };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Strip legacy unmarked TLC autoupdate entries (match our specific path)
|
|
285
|
+
const cleaned = existing
|
|
286
|
+
.split('\n')
|
|
287
|
+
.filter(line => !line.includes(SCRIPT_PATH))
|
|
288
|
+
.join('\n');
|
|
289
|
+
|
|
290
|
+
const entry = generateCronEntry(SCRIPT_PATH);
|
|
291
|
+
const newCrontab = cleaned.trimEnd() + (cleaned.trim() ? '\n' : '') + entry + '\n';
|
|
292
|
+
execSync('crontab -', { input: newCrontab, encoding: 'utf8' });
|
|
127
293
|
|
|
128
294
|
return { type: 'cron', scriptPath: SCRIPT_PATH };
|
|
129
295
|
}
|
|
130
296
|
|
|
131
297
|
/**
|
|
132
|
-
* Disables the auto-update
|
|
298
|
+
* Disables the auto-update scheduler.
|
|
299
|
+
* macOS: removes launchd plist
|
|
300
|
+
* Linux: removes cron entry
|
|
133
301
|
* @param {object} opts
|
|
134
302
|
* @param {string} opts.platform - 'darwin' or 'linux'
|
|
135
|
-
* @param {object} opts.fs - fs module (
|
|
136
|
-
* @param {Function} [opts.execSync] - child_process.execSync (
|
|
303
|
+
* @param {object} [opts.fs] - fs module (required for macOS)
|
|
304
|
+
* @param {Function} [opts.execSync] - child_process.execSync (required for Linux)
|
|
137
305
|
* @returns {{ removed: boolean }}
|
|
138
306
|
*/
|
|
139
307
|
export function disable({ platform, fs, execSync }) {
|
|
140
308
|
if (platform === 'darwin') {
|
|
141
|
-
if (fs.existsSync(
|
|
142
|
-
fs.unlinkSync(
|
|
309
|
+
if (fs && fs.existsSync(LAUNCHD_PLIST)) {
|
|
310
|
+
fs.unlinkSync(LAUNCHD_PLIST);
|
|
143
311
|
return { removed: true };
|
|
144
312
|
}
|
|
145
313
|
return { removed: false };
|
|
146
314
|
}
|
|
147
315
|
|
|
148
|
-
// Linux
|
|
316
|
+
// Linux: cron
|
|
149
317
|
let existing = '';
|
|
150
318
|
try {
|
|
151
319
|
existing = execSync('crontab -l 2>/dev/null || true', { encoding: 'utf8' });
|
|
152
|
-
} catch
|
|
153
|
-
|
|
320
|
+
} catch {
|
|
321
|
+
return { removed: false };
|
|
154
322
|
}
|
|
323
|
+
|
|
324
|
+
const hasMarked = existing.includes(CRON_MARKER);
|
|
325
|
+
const hasLegacy = !hasMarked && existing.includes(SCRIPT_PATH);
|
|
326
|
+
|
|
327
|
+
if (!hasMarked && !hasLegacy) {
|
|
328
|
+
return { removed: false };
|
|
329
|
+
}
|
|
330
|
+
|
|
155
331
|
const filtered = existing
|
|
156
332
|
.split('\n')
|
|
157
|
-
.filter(line => !line.includes(
|
|
333
|
+
.filter(line => !line.includes(CRON_MARKER) && !line.includes(SCRIPT_PATH))
|
|
158
334
|
.join('\n');
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
// best effort
|
|
166
|
-
}
|
|
335
|
+
|
|
336
|
+
const cleanCrontab = filtered.trim() ? filtered.trim() + '\n' : '';
|
|
337
|
+
if (cleanCrontab) {
|
|
338
|
+
execSync('crontab -', { input: cleanCrontab, encoding: 'utf8' });
|
|
339
|
+
} else {
|
|
340
|
+
execSync('crontab -r 2>/dev/null || true');
|
|
167
341
|
}
|
|
342
|
+
|
|
168
343
|
return { removed: true };
|
|
169
344
|
}
|
|
170
345
|
|
|
@@ -204,3 +379,57 @@ export function isStale(timestamp) {
|
|
|
204
379
|
const age = Date.now() - new Date(timestamp).getTime();
|
|
205
380
|
return age > 24 * 60 * 60 * 1000;
|
|
206
381
|
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Compares installed TLC version against latest on npm.
|
|
385
|
+
* Writes a notification file if an update is available, removes it if up to date.
|
|
386
|
+
* Designed to be called from the SessionStart hook for instant feedback.
|
|
387
|
+
* @param {object} opts
|
|
388
|
+
* @param {Function} opts.execSync - child_process.execSync
|
|
389
|
+
* @param {string} opts.installedVersion - current TLC version
|
|
390
|
+
* @param {object} [opts.fs] - fs module (for writing notification file)
|
|
391
|
+
* @param {string} [opts.dir] - directory for the notification file
|
|
392
|
+
* @returns {{ current: string, latest: string } | null}
|
|
393
|
+
*/
|
|
394
|
+
export function checkForUpdate({ execSync, installedVersion, fs, dir }) {
|
|
395
|
+
let latest;
|
|
396
|
+
try {
|
|
397
|
+
latest = execSync(`npm show ${TLC_PACKAGE} version 2>/dev/null`, { encoding: 'utf8' }).trim();
|
|
398
|
+
} catch {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (!latest || latest === installedVersion) {
|
|
403
|
+
// Up to date — remove stale notification if present
|
|
404
|
+
if (fs && dir) {
|
|
405
|
+
const notifPath = path.join(dir, UPDATE_AVAILABLE_FILE);
|
|
406
|
+
if (fs.existsSync && fs.existsSync(notifPath)) {
|
|
407
|
+
fs.unlinkSync(notifPath);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Simple semver compare: split and compare major.minor.patch
|
|
414
|
+
const parse = (v) => v.split('.').map(Number);
|
|
415
|
+
const [cMaj, cMin, cPat] = parse(installedVersion);
|
|
416
|
+
const [lMaj, lMin, lPat] = parse(latest);
|
|
417
|
+
const isNewer = lMaj > cMaj || (lMaj === cMaj && lMin > cMin) || (lMaj === cMaj && lMin === cMin && lPat > cPat);
|
|
418
|
+
|
|
419
|
+
if (!isNewer) {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Write notification file for SessionStart hook to display
|
|
424
|
+
if (fs && dir) {
|
|
425
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
426
|
+
const msg = [
|
|
427
|
+
`current=${installedVersion}`,
|
|
428
|
+
`latest=${latest}`,
|
|
429
|
+
`command=npm update -g ${TLC_PACKAGE}`,
|
|
430
|
+
].join('\n');
|
|
431
|
+
fs.writeFileSync(path.join(dir, UPDATE_AVAILABLE_FILE), msg, 'utf8');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return { current: installedVersion, latest };
|
|
435
|
+
}
|