@jojonax/codex-copilot 1.5.5 → 1.6.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/LICENSE +21 -21
- package/README.md +144 -44
- package/bin/cli.js +189 -182
- package/package.json +39 -39
- package/src/commands/evolve.js +316 -316
- package/src/commands/fix.js +447 -447
- package/src/commands/init.js +298 -298
- package/src/commands/reset.js +61 -61
- package/src/commands/retry.js +190 -190
- package/src/commands/run.js +958 -958
- package/src/commands/skip.js +62 -62
- package/src/commands/status.js +95 -95
- package/src/commands/usage.js +361 -361
- package/src/utils/automator.js +279 -279
- package/src/utils/checkpoint.js +246 -246
- package/src/utils/detect-prd.js +137 -137
- package/src/utils/git.js +388 -388
- package/src/utils/github.js +486 -486
- package/src/utils/json.js +220 -220
- package/src/utils/logger.js +41 -41
- package/src/utils/prompt.js +49 -49
- package/src/utils/provider.js +770 -769
- package/src/utils/self-heal.js +330 -330
- package/src/utils/shell-bootstrap.js +404 -0
- package/src/utils/update-check.js +103 -103
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell Bootstrap — ensures `sh` (POSIX shell) is available on Windows.
|
|
3
|
+
*
|
|
4
|
+
* Strategy (one-time check at CLI startup):
|
|
5
|
+
* 1. Try `sh --version` — if works, done.
|
|
6
|
+
* 2. Scan known Git-for-Windows paths for sh.exe.
|
|
7
|
+
* 3. If found, prepend its directory to process.env.PATH.
|
|
8
|
+
* 4. If not found, AUTO-INSTALL Git for Windows:
|
|
9
|
+
* a) Try winget (built into Windows 10/11)
|
|
10
|
+
* b) Fallback: download installer via PowerShell + silent install
|
|
11
|
+
* 5. After install, re-scan and configure PATH.
|
|
12
|
+
*
|
|
13
|
+
* On non-Windows platforms this module is a no-op.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { execSync } from 'child_process';
|
|
17
|
+
import { existsSync } from 'fs';
|
|
18
|
+
import { dirname } from 'path';
|
|
19
|
+
import { createInterface } from 'readline';
|
|
20
|
+
import { log } from './logger.js';
|
|
21
|
+
|
|
22
|
+
// ──────────────────────────────────────────────
|
|
23
|
+
// Known Git-for-Windows sh.exe locations
|
|
24
|
+
// ──────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
const GIT_SH_CANDIDATES = [
|
|
27
|
+
// 64-bit default
|
|
28
|
+
'C:\\Program Files\\Git\\bin\\sh.exe',
|
|
29
|
+
'C:\\Program Files\\Git\\usr\\bin\\sh.exe',
|
|
30
|
+
// 32-bit
|
|
31
|
+
'C:\\Program Files (x86)\\Git\\bin\\sh.exe',
|
|
32
|
+
'C:\\Program Files (x86)\\Git\\usr\\bin\\sh.exe',
|
|
33
|
+
// Scoop
|
|
34
|
+
`${process.env.USERPROFILE}\\scoop\\apps\\git\\current\\bin\\sh.exe`,
|
|
35
|
+
`${process.env.USERPROFILE}\\scoop\\apps\\git\\current\\usr\\bin\\sh.exe`,
|
|
36
|
+
// Chocolatey
|
|
37
|
+
'C:\\ProgramData\\chocolatey\\lib\\git\\tools\\bin\\sh.exe',
|
|
38
|
+
// Custom common paths
|
|
39
|
+
'C:\\Git\\bin\\sh.exe',
|
|
40
|
+
'D:\\Git\\bin\\sh.exe',
|
|
41
|
+
'D:\\Program Files\\Git\\bin\\sh.exe',
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// ──────────────────────────────────────────────
|
|
45
|
+
// Detection helpers
|
|
46
|
+
// ──────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if `sh` is callable from the current PATH.
|
|
50
|
+
*/
|
|
51
|
+
function isShAvailable() {
|
|
52
|
+
try {
|
|
53
|
+
execSync('sh --version', {
|
|
54
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
55
|
+
timeout: 5000,
|
|
56
|
+
});
|
|
57
|
+
return true;
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Scan well-known paths for sh.exe.
|
|
65
|
+
* Also checks GIT_INSTALL_ROOT env var and `where git` output.
|
|
66
|
+
* @returns {string|null} Full path to sh.exe, or null
|
|
67
|
+
*/
|
|
68
|
+
function findShExe() {
|
|
69
|
+
// Check GIT_INSTALL_ROOT env var first
|
|
70
|
+
const gitRoot = process.env.GIT_INSTALL_ROOT;
|
|
71
|
+
if (gitRoot) {
|
|
72
|
+
for (const sub of ['\\bin\\sh.exe', '\\usr\\bin\\sh.exe']) {
|
|
73
|
+
const p = gitRoot + sub;
|
|
74
|
+
if (existsSync(p)) return p;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Discover via `where git` command
|
|
79
|
+
try {
|
|
80
|
+
const gitPath = execSync('where git', {
|
|
81
|
+
encoding: 'utf-8',
|
|
82
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
83
|
+
timeout: 5000,
|
|
84
|
+
}).trim().split('\n')[0].trim();
|
|
85
|
+
if (gitPath) {
|
|
86
|
+
const gitDir = dirname(dirname(gitPath)); // up from cmd/git.exe
|
|
87
|
+
for (const sub of ['\\bin\\sh.exe', '\\usr\\bin\\sh.exe']) {
|
|
88
|
+
const p = gitDir + sub;
|
|
89
|
+
if (existsSync(p)) return p;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
// `git` not in PATH either
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Scan hardcoded candidate paths
|
|
97
|
+
for (const p of GIT_SH_CANDIDATES) {
|
|
98
|
+
if (existsSync(p)) return p;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Add sh.exe's directory (and usr/bin) to process.env.PATH.
|
|
106
|
+
* @param {string} shPath - Full path to sh.exe
|
|
107
|
+
* @returns {boolean} true if sh is now callable
|
|
108
|
+
*/
|
|
109
|
+
function addShToPath(shPath) {
|
|
110
|
+
const shDir = dirname(shPath);
|
|
111
|
+
process.env.PATH = `${shDir};${process.env.PATH}`;
|
|
112
|
+
|
|
113
|
+
// Also add usr/bin for other Unix tools (cat, grep, find, tail, etc.)
|
|
114
|
+
const usrBinDir = shDir.replace(/\\bin$/, '\\usr\\bin');
|
|
115
|
+
if (existsSync(usrBinDir) && usrBinDir !== shDir) {
|
|
116
|
+
process.env.PATH = `${usrBinDir};${process.env.PATH}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return isShAvailable();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ──────────────────────────────────────────────
|
|
123
|
+
// Auto-install
|
|
124
|
+
// ──────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Simple sync-style Y/n prompt (standalone, no dependency on prompt.js
|
|
128
|
+
* which creates a persistent readline that interferes with later prompts).
|
|
129
|
+
*/
|
|
130
|
+
function askYesNo(question) {
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
133
|
+
rl.question(` ${question} (Y/n): `, (answer) => {
|
|
134
|
+
rl.close();
|
|
135
|
+
const a = answer.trim().toLowerCase();
|
|
136
|
+
resolve(a === '' || a === 'y' || a === 'yes');
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Check if winget is available.
|
|
143
|
+
*/
|
|
144
|
+
function hasWinget() {
|
|
145
|
+
try {
|
|
146
|
+
execSync('winget --version', {
|
|
147
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
148
|
+
timeout: 10000,
|
|
149
|
+
});
|
|
150
|
+
return true;
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Install Git for Windows via winget (built into Windows 10/11).
|
|
158
|
+
* @returns {boolean} true if installation succeeded
|
|
159
|
+
*/
|
|
160
|
+
function installViaWinget() {
|
|
161
|
+
log.info('📦 Installing Git for Windows via winget...');
|
|
162
|
+
log.dim(' (This may trigger a UAC elevation prompt)');
|
|
163
|
+
try {
|
|
164
|
+
execSync(
|
|
165
|
+
'winget install Git.Git --silent --accept-source-agreements --accept-package-agreements',
|
|
166
|
+
{
|
|
167
|
+
stdio: 'inherit', // Show progress to user
|
|
168
|
+
timeout: 300000, // 5 minute timeout
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
log.info('✅ Git for Windows installed via winget');
|
|
172
|
+
return true;
|
|
173
|
+
} catch (err) {
|
|
174
|
+
log.warn(`winget install failed: ${(err.message || '').substring(0, 100)}`);
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Install Git for Windows by downloading the installer directly.
|
|
181
|
+
* Uses PowerShell to download and run the installer silently.
|
|
182
|
+
* @returns {boolean} true if installation succeeded
|
|
183
|
+
*/
|
|
184
|
+
function installViaDownload() {
|
|
185
|
+
log.info('📦 Downloading Git for Windows installer...');
|
|
186
|
+
|
|
187
|
+
// Step 1: Query GitHub API for the latest Git for Windows release URL
|
|
188
|
+
const apiUrl = 'https://api.github.com/repos/git-for-windows/git/releases/latest';
|
|
189
|
+
let downloadUrl;
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const releaseJson = execSync(
|
|
193
|
+
`powershell -NoProfile -Command "(Invoke-RestMethod -Uri '${apiUrl}').assets | Where-Object { $_.name -match '64-bit\\.exe$' } | Select-Object -First 1 -ExpandProperty browser_download_url"`,
|
|
194
|
+
{
|
|
195
|
+
encoding: 'utf-8',
|
|
196
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
197
|
+
timeout: 30000,
|
|
198
|
+
}
|
|
199
|
+
).trim();
|
|
200
|
+
|
|
201
|
+
if (releaseJson && releaseJson.startsWith('http')) {
|
|
202
|
+
downloadUrl = releaseJson;
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
// Fallback to a known stable URL pattern
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Fallback URL if API query failed
|
|
209
|
+
if (!downloadUrl) {
|
|
210
|
+
downloadUrl = 'https://github.com/git-for-windows/git/releases/latest/download/Git-2.47.1-64-bit.exe';
|
|
211
|
+
log.dim(' Using fallback download URL');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const installerPath = `${process.env.TEMP}\\git-installer.exe`;
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
// Step 2: Download
|
|
218
|
+
log.dim(` ↓ ${downloadUrl.substring(0, 80)}...`);
|
|
219
|
+
execSync(
|
|
220
|
+
`powershell -NoProfile -Command "` +
|
|
221
|
+
`[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; ` +
|
|
222
|
+
`$ProgressPreference = 'SilentlyContinue'; ` +
|
|
223
|
+
`Invoke-WebRequest -Uri '${downloadUrl}' -OutFile '${installerPath}' -UseBasicParsing"`,
|
|
224
|
+
{
|
|
225
|
+
stdio: 'inherit', // Show progress
|
|
226
|
+
timeout: 300000, // 5 min for download
|
|
227
|
+
}
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
if (!existsSync(installerPath)) {
|
|
231
|
+
log.warn('Download failed — installer file not found');
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Step 3: Run silent install
|
|
236
|
+
log.info('⚙️ Running installer (silent mode)...');
|
|
237
|
+
log.dim(' (This may trigger a UAC elevation prompt)');
|
|
238
|
+
execSync(
|
|
239
|
+
`"${installerPath}" /VERYSILENT /NORESTART /NOCANCEL /SP- /CLOSEAPPLICATIONS /RESTARTAPPLICATIONS /COMPONENTS="icons,ext\\shellhere,ext\\guihere,gitlfs,assoc,assoc_sh,autoupdate"`,
|
|
240
|
+
{
|
|
241
|
+
stdio: 'inherit',
|
|
242
|
+
timeout: 300000, // 5 min for install
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
log.info('✅ Git for Windows installed');
|
|
247
|
+
return true;
|
|
248
|
+
} catch (err) {
|
|
249
|
+
log.warn(`Direct install failed: ${(err.message || '').substring(0, 100)}`);
|
|
250
|
+
return false;
|
|
251
|
+
} finally {
|
|
252
|
+
// Cleanup installer
|
|
253
|
+
try {
|
|
254
|
+
if (existsSync(installerPath)) {
|
|
255
|
+
execSync(`del "${installerPath}"`, { stdio: 'pipe' });
|
|
256
|
+
}
|
|
257
|
+
} catch { /* ignore */ }
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Refresh PATH from the Windows registry after a new installation.
|
|
263
|
+
* New installs add to the system/user PATH in the registry,
|
|
264
|
+
* but the current process still has the old PATH.
|
|
265
|
+
*/
|
|
266
|
+
function refreshPathFromRegistry() {
|
|
267
|
+
try {
|
|
268
|
+
// Read both Machine and User PATH from registry and merge
|
|
269
|
+
const newPath = execSync(
|
|
270
|
+
`powershell -NoProfile -Command "` +
|
|
271
|
+
`$machine = [Environment]::GetEnvironmentVariable('Path', 'Machine'); ` +
|
|
272
|
+
`$user = [Environment]::GetEnvironmentVariable('Path', 'User'); ` +
|
|
273
|
+
`Write-Output ($machine + ';' + $user)"`,
|
|
274
|
+
{
|
|
275
|
+
encoding: 'utf-8',
|
|
276
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
277
|
+
timeout: 10000,
|
|
278
|
+
}
|
|
279
|
+
).trim();
|
|
280
|
+
|
|
281
|
+
if (newPath) {
|
|
282
|
+
process.env.PATH = newPath;
|
|
283
|
+
log.dim(' PATH refreshed from registry');
|
|
284
|
+
}
|
|
285
|
+
} catch {
|
|
286
|
+
// Non-fatal — we'll still try to find sh.exe by scanning
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Attempt to auto-install Git for Windows.
|
|
292
|
+
* Tries winget first, then falls back to direct download.
|
|
293
|
+
* @returns {boolean} true if sh is now available
|
|
294
|
+
*/
|
|
295
|
+
async function autoInstallGit() {
|
|
296
|
+
log.blank();
|
|
297
|
+
log.info('╔══════════════════════════════════════════════════╗');
|
|
298
|
+
log.info('║ POSIX shell (sh) not found on this system. ║');
|
|
299
|
+
log.info('║ codex-copilot needs sh for Unix commands. ║');
|
|
300
|
+
log.info('║ ║');
|
|
301
|
+
log.info('║ Git for Windows includes sh.exe + cat, grep, ║');
|
|
302
|
+
log.info('║ find, tail, and other required tools. ║');
|
|
303
|
+
log.info('╚══════════════════════════════════════════════════╝');
|
|
304
|
+
log.blank();
|
|
305
|
+
|
|
306
|
+
const yes = await askYesNo('Auto-install Git for Windows now?');
|
|
307
|
+
if (!yes) {
|
|
308
|
+
log.dim(' Skipped. Install manually: https://git-scm.com/download/win');
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
log.blank();
|
|
313
|
+
|
|
314
|
+
// Strategy 1: winget (cleanest, built into modern Windows)
|
|
315
|
+
let installed = false;
|
|
316
|
+
if (hasWinget()) {
|
|
317
|
+
log.dim(' winget detected — using package manager');
|
|
318
|
+
installed = installViaWinget();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Strategy 2: Direct download + silent install
|
|
322
|
+
if (!installed) {
|
|
323
|
+
if (hasWinget()) {
|
|
324
|
+
log.dim(' winget failed — trying direct download...');
|
|
325
|
+
} else {
|
|
326
|
+
log.dim(' winget not available — trying direct download...');
|
|
327
|
+
}
|
|
328
|
+
installed = installViaDownload();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!installed) {
|
|
332
|
+
log.error('❌ Auto-install failed. Please install manually:');
|
|
333
|
+
log.error(' → https://git-scm.com/download/win');
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Refresh PATH from registry (picks up the new Git installation)
|
|
338
|
+
refreshPathFromRegistry();
|
|
339
|
+
|
|
340
|
+
// Re-scan for sh.exe
|
|
341
|
+
const shPath = findShExe();
|
|
342
|
+
if (shPath) {
|
|
343
|
+
addShToPath(shPath);
|
|
344
|
+
if (isShAvailable()) {
|
|
345
|
+
log.info(`✅ sh available at ${shPath}`);
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Last resort: try common post-install paths directly
|
|
351
|
+
const postInstallPaths = [
|
|
352
|
+
'C:\\Program Files\\Git\\bin\\sh.exe',
|
|
353
|
+
'C:\\Program Files\\Git\\usr\\bin\\sh.exe',
|
|
354
|
+
];
|
|
355
|
+
for (const p of postInstallPaths) {
|
|
356
|
+
if (existsSync(p)) {
|
|
357
|
+
addShToPath(p);
|
|
358
|
+
if (isShAvailable()) {
|
|
359
|
+
log.info(`✅ sh available at ${p}`);
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
log.warn('Git installed but sh.exe not found — please restart your terminal');
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ──────────────────────────────────────────────
|
|
370
|
+
// Public API
|
|
371
|
+
// ──────────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Bootstrap shell environment on Windows.
|
|
375
|
+
*
|
|
376
|
+
* Call this ONCE at CLI startup. It either:
|
|
377
|
+
* - Does nothing (sh already available, or not Windows)
|
|
378
|
+
* - Finds sh.exe and adds it to PATH for this process
|
|
379
|
+
* - Auto-installs Git for Windows and configures PATH
|
|
380
|
+
* - Returns false only if everything failed
|
|
381
|
+
*
|
|
382
|
+
* @returns {Promise<boolean>} true if sh is available after bootstrap
|
|
383
|
+
*/
|
|
384
|
+
export async function bootstrapShell() {
|
|
385
|
+
// Non-Windows — sh is always available
|
|
386
|
+
if (process.platform !== 'win32') return true;
|
|
387
|
+
|
|
388
|
+
// Already in PATH — great, nothing to do
|
|
389
|
+
if (isShAvailable()) return true;
|
|
390
|
+
|
|
391
|
+
log.dim(' sh not found in PATH — scanning for Git for Windows...');
|
|
392
|
+
|
|
393
|
+
// Try to find existing sh.exe installation
|
|
394
|
+
const shPath = findShExe();
|
|
395
|
+
if (shPath) {
|
|
396
|
+
if (addShToPath(shPath)) {
|
|
397
|
+
log.info(`✅ Found sh at ${shPath} — added to PATH`);
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Not found anywhere — offer to auto-install
|
|
403
|
+
return await autoInstallGit();
|
|
404
|
+
}
|
|
@@ -1,103 +1,103 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Version update checker
|
|
3
|
-
*
|
|
4
|
-
* - Checks npm registry for latest version on startup
|
|
5
|
-
* - 24h cache to avoid frequent network requests
|
|
6
|
-
* - Prints update prompt if a newer version is available
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { execSync } from 'child_process';
|
|
10
|
-
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
11
|
-
import { resolve } from 'path';
|
|
12
|
-
import { homedir } from 'os';
|
|
13
|
-
import { log } from './logger.js';
|
|
14
|
-
|
|
15
|
-
const PACKAGE_NAME = '@jojonax/codex-copilot';
|
|
16
|
-
const CACHE_FILE = resolve(homedir(), '.codex-copilot-update-cache.json');
|
|
17
|
-
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Read cached version info
|
|
21
|
-
*/
|
|
22
|
-
function readCache() {
|
|
23
|
-
try {
|
|
24
|
-
const data = JSON.parse(readFileSync(CACHE_FILE, 'utf-8'));
|
|
25
|
-
if (Date.now() - data.timestamp < CACHE_TTL) {
|
|
26
|
-
return data.latestVersion;
|
|
27
|
-
}
|
|
28
|
-
} catch {
|
|
29
|
-
// Cache miss or corrupt — ignore
|
|
30
|
-
}
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Write version info to cache
|
|
36
|
-
*/
|
|
37
|
-
function writeCache(latestVersion) {
|
|
38
|
-
try {
|
|
39
|
-
writeFileSync(CACHE_FILE, JSON.stringify({
|
|
40
|
-
latestVersion,
|
|
41
|
-
timestamp: Date.now(),
|
|
42
|
-
}));
|
|
43
|
-
} catch {
|
|
44
|
-
// Permission error — ignore silently
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Fetch latest version from npm registry
|
|
50
|
-
*/
|
|
51
|
-
function fetchLatestVersion() {
|
|
52
|
-
try {
|
|
53
|
-
const output = execSync(`npm view ${PACKAGE_NAME} version`, {
|
|
54
|
-
encoding: 'utf-8',
|
|
55
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
56
|
-
timeout: 5000, // 5s timeout
|
|
57
|
-
}).trim();
|
|
58
|
-
return output || null;
|
|
59
|
-
} catch {
|
|
60
|
-
// Network error, npm not available — ignore
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Compare semantic version strings
|
|
67
|
-
* @returns {boolean} true if latest > current
|
|
68
|
-
*/
|
|
69
|
-
function isNewer(current, latest) {
|
|
70
|
-
if (!current || !latest) return false;
|
|
71
|
-
const c = current.split('.').map(Number);
|
|
72
|
-
const l = latest.split('.').map(Number);
|
|
73
|
-
for (let i = 0; i < 3; i++) {
|
|
74
|
-
if ((l[i] || 0) > (c[i] || 0)) return true;
|
|
75
|
-
if ((l[i] || 0) < (c[i] || 0)) return false;
|
|
76
|
-
}
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Check for updates and print notification
|
|
82
|
-
* @param {string} currentVersion - Current installed version
|
|
83
|
-
*/
|
|
84
|
-
export function checkForUpdates(currentVersion) {
|
|
85
|
-
// 1. Check cache first
|
|
86
|
-
let latest = readCache();
|
|
87
|
-
|
|
88
|
-
// 2. Cache miss → fetch from npm
|
|
89
|
-
if (!latest) {
|
|
90
|
-
latest = fetchLatestVersion();
|
|
91
|
-
if (latest) {
|
|
92
|
-
writeCache(latest);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// 3. Compare and notify
|
|
97
|
-
if (latest && isNewer(currentVersion, latest)) {
|
|
98
|
-
log.blank();
|
|
99
|
-
log.warn(`Update available: v${currentVersion} → v${latest}`);
|
|
100
|
-
log.dim(` Run the following command to update:`);
|
|
101
|
-
log.dim(` npm install -g ${PACKAGE_NAME}@${latest}`);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Version update checker
|
|
3
|
+
*
|
|
4
|
+
* - Checks npm registry for latest version on startup
|
|
5
|
+
* - 24h cache to avoid frequent network requests
|
|
6
|
+
* - Prints update prompt if a newer version is available
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
11
|
+
import { resolve } from 'path';
|
|
12
|
+
import { homedir } from 'os';
|
|
13
|
+
import { log } from './logger.js';
|
|
14
|
+
|
|
15
|
+
const PACKAGE_NAME = '@jojonax/codex-copilot';
|
|
16
|
+
const CACHE_FILE = resolve(homedir(), '.codex-copilot-update-cache.json');
|
|
17
|
+
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Read cached version info
|
|
21
|
+
*/
|
|
22
|
+
function readCache() {
|
|
23
|
+
try {
|
|
24
|
+
const data = JSON.parse(readFileSync(CACHE_FILE, 'utf-8'));
|
|
25
|
+
if (Date.now() - data.timestamp < CACHE_TTL) {
|
|
26
|
+
return data.latestVersion;
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
// Cache miss or corrupt — ignore
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Write version info to cache
|
|
36
|
+
*/
|
|
37
|
+
function writeCache(latestVersion) {
|
|
38
|
+
try {
|
|
39
|
+
writeFileSync(CACHE_FILE, JSON.stringify({
|
|
40
|
+
latestVersion,
|
|
41
|
+
timestamp: Date.now(),
|
|
42
|
+
}));
|
|
43
|
+
} catch {
|
|
44
|
+
// Permission error — ignore silently
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Fetch latest version from npm registry
|
|
50
|
+
*/
|
|
51
|
+
function fetchLatestVersion() {
|
|
52
|
+
try {
|
|
53
|
+
const output = execSync(`npm view ${PACKAGE_NAME} version`, {
|
|
54
|
+
encoding: 'utf-8',
|
|
55
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
56
|
+
timeout: 5000, // 5s timeout
|
|
57
|
+
}).trim();
|
|
58
|
+
return output || null;
|
|
59
|
+
} catch {
|
|
60
|
+
// Network error, npm not available — ignore
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Compare semantic version strings
|
|
67
|
+
* @returns {boolean} true if latest > current
|
|
68
|
+
*/
|
|
69
|
+
function isNewer(current, latest) {
|
|
70
|
+
if (!current || !latest) return false;
|
|
71
|
+
const c = current.split('.').map(Number);
|
|
72
|
+
const l = latest.split('.').map(Number);
|
|
73
|
+
for (let i = 0; i < 3; i++) {
|
|
74
|
+
if ((l[i] || 0) > (c[i] || 0)) return true;
|
|
75
|
+
if ((l[i] || 0) < (c[i] || 0)) return false;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check for updates and print notification
|
|
82
|
+
* @param {string} currentVersion - Current installed version
|
|
83
|
+
*/
|
|
84
|
+
export function checkForUpdates(currentVersion) {
|
|
85
|
+
// 1. Check cache first
|
|
86
|
+
let latest = readCache();
|
|
87
|
+
|
|
88
|
+
// 2. Cache miss → fetch from npm
|
|
89
|
+
if (!latest) {
|
|
90
|
+
latest = fetchLatestVersion();
|
|
91
|
+
if (latest) {
|
|
92
|
+
writeCache(latest);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 3. Compare and notify
|
|
97
|
+
if (latest && isNewer(currentVersion, latest)) {
|
|
98
|
+
log.blank();
|
|
99
|
+
log.warn(`Update available: v${currentVersion} → v${latest}`);
|
|
100
|
+
log.dim(` Run the following command to update:`);
|
|
101
|
+
log.dim(` npm install -g ${PACKAGE_NAME}@${latest}`);
|
|
102
|
+
}
|
|
103
|
+
}
|