@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.
@@ -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
+ }