@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
package/src/utils/automator.js
CHANGED
|
@@ -1,279 +1,279 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Desktop IDE Automator — platform-aware process detection + auto-paste
|
|
3
|
-
*
|
|
4
|
-
* Supports macOS (AppleScript/osascript) and Windows (PowerShell/SendKeys).
|
|
5
|
-
* Each IDE has its own keystroke recipe.
|
|
6
|
-
* Falls back gracefully if automation fails.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { execSync } from 'child_process';
|
|
10
|
-
import { log } from './logger.js';
|
|
11
|
-
|
|
12
|
-
const IS_MAC = process.platform === 'darwin';
|
|
13
|
-
const IS_WIN = process.platform === 'win32';
|
|
14
|
-
|
|
15
|
-
// ──────────────────────────────────────────────
|
|
16
|
-
// Per-IDE Automation Recipes
|
|
17
|
-
// ──────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Each recipe defines:
|
|
21
|
-
* processName: { mac, win } — binary/process name to detect
|
|
22
|
-
* appName: { mac, win } — display name for activation
|
|
23
|
-
* preKeys: keystroke sequence BEFORE paste (e.g., open Composer in Cursor)
|
|
24
|
-
* preDelay: ms to wait after preKeys
|
|
25
|
-
* postKeys: keystroke sequence AFTER paste (usually Enter to send)
|
|
26
|
-
* pasteDelay: ms to wait between paste and postKeys
|
|
27
|
-
*/
|
|
28
|
-
const IDE_RECIPES = {
|
|
29
|
-
'codex-desktop': {
|
|
30
|
-
processName: { mac: 'Codex', win: 'Codex' },
|
|
31
|
-
appName: { mac: 'Codex', win: 'Codex' },
|
|
32
|
-
// Codex Desktop: input is focused on activate → paste → Enter
|
|
33
|
-
preKeys: null,
|
|
34
|
-
preDelay: 0,
|
|
35
|
-
postKeys: 'return',
|
|
36
|
-
pasteDelay: 300,
|
|
37
|
-
},
|
|
38
|
-
|
|
39
|
-
'cursor': {
|
|
40
|
-
processName: { mac: 'Cursor', win: 'Cursor' },
|
|
41
|
-
appName: { mac: 'Cursor', win: 'Cursor' },
|
|
42
|
-
// Cursor: Cmd/Ctrl+I opens Composer, then paste, then Enter
|
|
43
|
-
preKeys: IS_MAC
|
|
44
|
-
? 'keystroke "i" using {command down}'
|
|
45
|
-
: '^i',
|
|
46
|
-
preDelay: 800,
|
|
47
|
-
postKeys: 'return',
|
|
48
|
-
pasteDelay: 300,
|
|
49
|
-
},
|
|
50
|
-
|
|
51
|
-
'antigravity': {
|
|
52
|
-
processName: { mac: 'Antigravity', win: 'Antigravity' },
|
|
53
|
-
appName: { mac: 'Antigravity', win: 'Antigravity' },
|
|
54
|
-
// Antigravity: chat input auto-focused → paste → Enter
|
|
55
|
-
preKeys: null,
|
|
56
|
-
preDelay: 0,
|
|
57
|
-
postKeys: 'return',
|
|
58
|
-
pasteDelay: 300,
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
// ──────────────────────────────────────────────
|
|
63
|
-
// Process Detection
|
|
64
|
-
// ──────────────────────────────────────────────
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Check if a process is running
|
|
68
|
-
* @param {string} processName - Name of the process
|
|
69
|
-
* @returns {boolean}
|
|
70
|
-
*/
|
|
71
|
-
export function isProcessRunning(processName) {
|
|
72
|
-
try {
|
|
73
|
-
if (IS_MAC) {
|
|
74
|
-
const result = execSync(
|
|
75
|
-
`pgrep -x "${processName}" || pgrep -fi "${processName}"`,
|
|
76
|
-
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
77
|
-
).trim();
|
|
78
|
-
return result.length > 0;
|
|
79
|
-
} else if (IS_WIN) {
|
|
80
|
-
const result = execSync(
|
|
81
|
-
`tasklist /FI "IMAGENAME eq ${processName}.exe" /NH`,
|
|
82
|
-
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
83
|
-
).trim();
|
|
84
|
-
return !result.includes('No tasks');
|
|
85
|
-
}
|
|
86
|
-
return false;
|
|
87
|
-
} catch {
|
|
88
|
-
return false;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ──────────────────────────────────────────────
|
|
93
|
-
// macOS Automation (AppleScript)
|
|
94
|
-
// ──────────────────────────────────────────────
|
|
95
|
-
|
|
96
|
-
function buildAppleScript(recipe) {
|
|
97
|
-
const appName = recipe.appName.mac;
|
|
98
|
-
const lines = [];
|
|
99
|
-
|
|
100
|
-
// Activate the application
|
|
101
|
-
lines.push(`tell application "${appName}" to activate`);
|
|
102
|
-
lines.push('delay 0.5');
|
|
103
|
-
|
|
104
|
-
// System Events block for keystrokes
|
|
105
|
-
lines.push('tell application "System Events"');
|
|
106
|
-
|
|
107
|
-
// Pre-keys (e.g., Cmd+I for Cursor Composer)
|
|
108
|
-
if (recipe.preKeys) {
|
|
109
|
-
lines.push(` ${recipe.preKeys}`);
|
|
110
|
-
lines.push(` delay ${recipe.preDelay / 1000}`);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Paste: Cmd+V
|
|
114
|
-
lines.push(' keystroke "v" using {command down}');
|
|
115
|
-
lines.push(` delay ${recipe.pasteDelay / 1000}`);
|
|
116
|
-
|
|
117
|
-
// Post-keys: usually Enter to send
|
|
118
|
-
if (recipe.postKeys === 'return') {
|
|
119
|
-
lines.push(' keystroke return');
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
lines.push('end tell');
|
|
123
|
-
|
|
124
|
-
return lines.join('\n');
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function runAppleScript(script) {
|
|
128
|
-
try {
|
|
129
|
-
execSync(`osascript -e '${script.replace(/'/g, "'\"'\"'")}'`, {
|
|
130
|
-
encoding: 'utf-8',
|
|
131
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
132
|
-
timeout: 10000,
|
|
133
|
-
});
|
|
134
|
-
return true;
|
|
135
|
-
} catch (err) {
|
|
136
|
-
log.warn(`AppleScript failed: ${err.message}`);
|
|
137
|
-
if (err.message.includes('not allowed assistive access')) {
|
|
138
|
-
log.error('⚠ Accessibility permission required:');
|
|
139
|
-
log.error(' System Settings → Privacy & Security → Accessibility');
|
|
140
|
-
log.error(' Add your terminal app (Terminal / iTerm2 / Warp)');
|
|
141
|
-
}
|
|
142
|
-
return false;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// ──────────────────────────────────────────────
|
|
147
|
-
// Windows Automation (PowerShell)
|
|
148
|
-
// ──────────────────────────────────────────────
|
|
149
|
-
|
|
150
|
-
function buildPowerShell(recipe) {
|
|
151
|
-
const appName = recipe.appName.win;
|
|
152
|
-
const lines = [];
|
|
153
|
-
|
|
154
|
-
lines.push('Add-Type -AssemblyName System.Windows.Forms');
|
|
155
|
-
lines.push('Add-Type -AssemblyName Microsoft.VisualBasic');
|
|
156
|
-
|
|
157
|
-
// Activate window
|
|
158
|
-
lines.push(`[Microsoft.VisualBasic.Interaction]::AppActivate("${appName}")`);
|
|
159
|
-
lines.push('Start-Sleep -Milliseconds 500');
|
|
160
|
-
|
|
161
|
-
// Pre-keys
|
|
162
|
-
if (recipe.preKeys) {
|
|
163
|
-
lines.push(`[System.Windows.Forms.SendKeys]::SendWait("${recipe.preKeys}")`);
|
|
164
|
-
lines.push(`Start-Sleep -Milliseconds ${recipe.preDelay}`);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Paste: Ctrl+V
|
|
168
|
-
lines.push('[System.Windows.Forms.SendKeys]::SendWait("^v")');
|
|
169
|
-
lines.push(`Start-Sleep -Milliseconds ${recipe.pasteDelay}`);
|
|
170
|
-
|
|
171
|
-
// Post-keys: Enter
|
|
172
|
-
if (recipe.postKeys === 'return') {
|
|
173
|
-
lines.push('[System.Windows.Forms.SendKeys]::SendWait("{ENTER}")');
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return lines.join('; ');
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function runPowerShell(script) {
|
|
180
|
-
try {
|
|
181
|
-
execSync(`powershell -NoProfile -Command "${script.replace(/"/g, '\\"')}"`, {
|
|
182
|
-
encoding: 'utf-8',
|
|
183
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
184
|
-
timeout: 10000,
|
|
185
|
-
});
|
|
186
|
-
return true;
|
|
187
|
-
} catch (err) {
|
|
188
|
-
log.warn(`PowerShell automation failed: ${err.message}`);
|
|
189
|
-
return false;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// ──────────────────────────────────────────────
|
|
194
|
-
// Public API
|
|
195
|
-
// ──────────────────────────────────────────────
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Check if a specific IDE has a known automation recipe
|
|
199
|
-
*/
|
|
200
|
-
export function hasRecipe(providerId) {
|
|
201
|
-
return providerId in IDE_RECIPES;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Check if the IDE is currently running
|
|
206
|
-
*/
|
|
207
|
-
export function isIDERunning(providerId) {
|
|
208
|
-
const recipe = IDE_RECIPES[providerId];
|
|
209
|
-
if (!recipe) return false;
|
|
210
|
-
|
|
211
|
-
const processName = IS_MAC ? recipe.processName.mac : recipe.processName.win;
|
|
212
|
-
return isProcessRunning(processName);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Attempt to auto-paste prompt into the IDE and trigger send
|
|
217
|
-
*
|
|
218
|
-
* Flow: copy to clipboard → activate window → pre-keys → paste → post-keys
|
|
219
|
-
*
|
|
220
|
-
* @param {string} providerId - IDE provider ID
|
|
221
|
-
* @param {string} text - Prompt text to paste
|
|
222
|
-
* @returns {boolean} true if automation succeeded
|
|
223
|
-
*/
|
|
224
|
-
export function activateAndPaste(providerId, text) {
|
|
225
|
-
const recipe = IDE_RECIPES[providerId];
|
|
226
|
-
if (!recipe) {
|
|
227
|
-
log.warn(`No automation recipe for '${providerId}'`);
|
|
228
|
-
return false;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Step 1: Check process is running
|
|
232
|
-
const processName = IS_MAC ? recipe.processName.mac : recipe.processName.win;
|
|
233
|
-
if (!isProcessRunning(processName)) {
|
|
234
|
-
log.warn(`${recipe.appName[IS_MAC ? 'mac' : 'win']} is not running`);
|
|
235
|
-
return false;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Step 2: Copy to clipboard (handled by caller, but ensure it's done)
|
|
239
|
-
copyToSystemClipboard(text);
|
|
240
|
-
|
|
241
|
-
// Step 3: Run platform-specific automation
|
|
242
|
-
log.info(`Auto-pasting into ${recipe.appName[IS_MAC ? 'mac' : 'win']}...`);
|
|
243
|
-
|
|
244
|
-
if (IS_MAC) {
|
|
245
|
-
const script = buildAppleScript(recipe);
|
|
246
|
-
return runAppleScript(script);
|
|
247
|
-
} else if (IS_WIN) {
|
|
248
|
-
const script = buildPowerShell(recipe);
|
|
249
|
-
return runPowerShell(script);
|
|
250
|
-
} else {
|
|
251
|
-
log.warn('Desktop automation not supported on this platform');
|
|
252
|
-
return false;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Copy text to system clipboard (cross-platform)
|
|
258
|
-
*/
|
|
259
|
-
function copyToSystemClipboard(text) {
|
|
260
|
-
try {
|
|
261
|
-
if (IS_MAC) {
|
|
262
|
-
execSync('pbcopy', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
263
|
-
} else if (IS_WIN) {
|
|
264
|
-
execSync('clip', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
265
|
-
} else {
|
|
266
|
-
try {
|
|
267
|
-
execSync('xclip -selection clipboard', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
268
|
-
} catch {
|
|
269
|
-
execSync('xsel --clipboard --input', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
} catch {
|
|
273
|
-
// Clipboard failure is non-fatal — user can copy file manually
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
export const automator = {
|
|
278
|
-
isProcessRunning, hasRecipe, isIDERunning, activateAndPaste,
|
|
279
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Desktop IDE Automator — platform-aware process detection + auto-paste
|
|
3
|
+
*
|
|
4
|
+
* Supports macOS (AppleScript/osascript) and Windows (PowerShell/SendKeys).
|
|
5
|
+
* Each IDE has its own keystroke recipe.
|
|
6
|
+
* Falls back gracefully if automation fails.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
import { log } from './logger.js';
|
|
11
|
+
|
|
12
|
+
const IS_MAC = process.platform === 'darwin';
|
|
13
|
+
const IS_WIN = process.platform === 'win32';
|
|
14
|
+
|
|
15
|
+
// ──────────────────────────────────────────────
|
|
16
|
+
// Per-IDE Automation Recipes
|
|
17
|
+
// ──────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Each recipe defines:
|
|
21
|
+
* processName: { mac, win } — binary/process name to detect
|
|
22
|
+
* appName: { mac, win } — display name for activation
|
|
23
|
+
* preKeys: keystroke sequence BEFORE paste (e.g., open Composer in Cursor)
|
|
24
|
+
* preDelay: ms to wait after preKeys
|
|
25
|
+
* postKeys: keystroke sequence AFTER paste (usually Enter to send)
|
|
26
|
+
* pasteDelay: ms to wait between paste and postKeys
|
|
27
|
+
*/
|
|
28
|
+
const IDE_RECIPES = {
|
|
29
|
+
'codex-desktop': {
|
|
30
|
+
processName: { mac: 'Codex', win: 'Codex' },
|
|
31
|
+
appName: { mac: 'Codex', win: 'Codex' },
|
|
32
|
+
// Codex Desktop: input is focused on activate → paste → Enter
|
|
33
|
+
preKeys: null,
|
|
34
|
+
preDelay: 0,
|
|
35
|
+
postKeys: 'return',
|
|
36
|
+
pasteDelay: 300,
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
'cursor': {
|
|
40
|
+
processName: { mac: 'Cursor', win: 'Cursor' },
|
|
41
|
+
appName: { mac: 'Cursor', win: 'Cursor' },
|
|
42
|
+
// Cursor: Cmd/Ctrl+I opens Composer, then paste, then Enter
|
|
43
|
+
preKeys: IS_MAC
|
|
44
|
+
? 'keystroke "i" using {command down}'
|
|
45
|
+
: '^i',
|
|
46
|
+
preDelay: 800,
|
|
47
|
+
postKeys: 'return',
|
|
48
|
+
pasteDelay: 300,
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
'antigravity': {
|
|
52
|
+
processName: { mac: 'Antigravity', win: 'Antigravity' },
|
|
53
|
+
appName: { mac: 'Antigravity', win: 'Antigravity' },
|
|
54
|
+
// Antigravity: chat input auto-focused → paste → Enter
|
|
55
|
+
preKeys: null,
|
|
56
|
+
preDelay: 0,
|
|
57
|
+
postKeys: 'return',
|
|
58
|
+
pasteDelay: 300,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// ──────────────────────────────────────────────
|
|
63
|
+
// Process Detection
|
|
64
|
+
// ──────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if a process is running
|
|
68
|
+
* @param {string} processName - Name of the process
|
|
69
|
+
* @returns {boolean}
|
|
70
|
+
*/
|
|
71
|
+
export function isProcessRunning(processName) {
|
|
72
|
+
try {
|
|
73
|
+
if (IS_MAC) {
|
|
74
|
+
const result = execSync(
|
|
75
|
+
`pgrep -x "${processName}" || pgrep -fi "${processName}"`,
|
|
76
|
+
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
77
|
+
).trim();
|
|
78
|
+
return result.length > 0;
|
|
79
|
+
} else if (IS_WIN) {
|
|
80
|
+
const result = execSync(
|
|
81
|
+
`tasklist /FI "IMAGENAME eq ${processName}.exe" /NH`,
|
|
82
|
+
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
83
|
+
).trim();
|
|
84
|
+
return !result.includes('No tasks');
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ──────────────────────────────────────────────
|
|
93
|
+
// macOS Automation (AppleScript)
|
|
94
|
+
// ──────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
function buildAppleScript(recipe) {
|
|
97
|
+
const appName = recipe.appName.mac;
|
|
98
|
+
const lines = [];
|
|
99
|
+
|
|
100
|
+
// Activate the application
|
|
101
|
+
lines.push(`tell application "${appName}" to activate`);
|
|
102
|
+
lines.push('delay 0.5');
|
|
103
|
+
|
|
104
|
+
// System Events block for keystrokes
|
|
105
|
+
lines.push('tell application "System Events"');
|
|
106
|
+
|
|
107
|
+
// Pre-keys (e.g., Cmd+I for Cursor Composer)
|
|
108
|
+
if (recipe.preKeys) {
|
|
109
|
+
lines.push(` ${recipe.preKeys}`);
|
|
110
|
+
lines.push(` delay ${recipe.preDelay / 1000}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Paste: Cmd+V
|
|
114
|
+
lines.push(' keystroke "v" using {command down}');
|
|
115
|
+
lines.push(` delay ${recipe.pasteDelay / 1000}`);
|
|
116
|
+
|
|
117
|
+
// Post-keys: usually Enter to send
|
|
118
|
+
if (recipe.postKeys === 'return') {
|
|
119
|
+
lines.push(' keystroke return');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
lines.push('end tell');
|
|
123
|
+
|
|
124
|
+
return lines.join('\n');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function runAppleScript(script) {
|
|
128
|
+
try {
|
|
129
|
+
execSync(`osascript -e '${script.replace(/'/g, "'\"'\"'")}'`, {
|
|
130
|
+
encoding: 'utf-8',
|
|
131
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
132
|
+
timeout: 10000,
|
|
133
|
+
});
|
|
134
|
+
return true;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
log.warn(`AppleScript failed: ${err.message}`);
|
|
137
|
+
if (err.message.includes('not allowed assistive access')) {
|
|
138
|
+
log.error('⚠ Accessibility permission required:');
|
|
139
|
+
log.error(' System Settings → Privacy & Security → Accessibility');
|
|
140
|
+
log.error(' Add your terminal app (Terminal / iTerm2 / Warp)');
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ──────────────────────────────────────────────
|
|
147
|
+
// Windows Automation (PowerShell)
|
|
148
|
+
// ──────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
function buildPowerShell(recipe) {
|
|
151
|
+
const appName = recipe.appName.win;
|
|
152
|
+
const lines = [];
|
|
153
|
+
|
|
154
|
+
lines.push('Add-Type -AssemblyName System.Windows.Forms');
|
|
155
|
+
lines.push('Add-Type -AssemblyName Microsoft.VisualBasic');
|
|
156
|
+
|
|
157
|
+
// Activate window
|
|
158
|
+
lines.push(`[Microsoft.VisualBasic.Interaction]::AppActivate("${appName}")`);
|
|
159
|
+
lines.push('Start-Sleep -Milliseconds 500');
|
|
160
|
+
|
|
161
|
+
// Pre-keys
|
|
162
|
+
if (recipe.preKeys) {
|
|
163
|
+
lines.push(`[System.Windows.Forms.SendKeys]::SendWait("${recipe.preKeys}")`);
|
|
164
|
+
lines.push(`Start-Sleep -Milliseconds ${recipe.preDelay}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Paste: Ctrl+V
|
|
168
|
+
lines.push('[System.Windows.Forms.SendKeys]::SendWait("^v")');
|
|
169
|
+
lines.push(`Start-Sleep -Milliseconds ${recipe.pasteDelay}`);
|
|
170
|
+
|
|
171
|
+
// Post-keys: Enter
|
|
172
|
+
if (recipe.postKeys === 'return') {
|
|
173
|
+
lines.push('[System.Windows.Forms.SendKeys]::SendWait("{ENTER}")');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return lines.join('; ');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function runPowerShell(script) {
|
|
180
|
+
try {
|
|
181
|
+
execSync(`powershell -NoProfile -Command "${script.replace(/"/g, '\\"')}"`, {
|
|
182
|
+
encoding: 'utf-8',
|
|
183
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
184
|
+
timeout: 10000,
|
|
185
|
+
});
|
|
186
|
+
return true;
|
|
187
|
+
} catch (err) {
|
|
188
|
+
log.warn(`PowerShell automation failed: ${err.message}`);
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ──────────────────────────────────────────────
|
|
194
|
+
// Public API
|
|
195
|
+
// ──────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Check if a specific IDE has a known automation recipe
|
|
199
|
+
*/
|
|
200
|
+
export function hasRecipe(providerId) {
|
|
201
|
+
return providerId in IDE_RECIPES;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Check if the IDE is currently running
|
|
206
|
+
*/
|
|
207
|
+
export function isIDERunning(providerId) {
|
|
208
|
+
const recipe = IDE_RECIPES[providerId];
|
|
209
|
+
if (!recipe) return false;
|
|
210
|
+
|
|
211
|
+
const processName = IS_MAC ? recipe.processName.mac : recipe.processName.win;
|
|
212
|
+
return isProcessRunning(processName);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Attempt to auto-paste prompt into the IDE and trigger send
|
|
217
|
+
*
|
|
218
|
+
* Flow: copy to clipboard → activate window → pre-keys → paste → post-keys
|
|
219
|
+
*
|
|
220
|
+
* @param {string} providerId - IDE provider ID
|
|
221
|
+
* @param {string} text - Prompt text to paste
|
|
222
|
+
* @returns {boolean} true if automation succeeded
|
|
223
|
+
*/
|
|
224
|
+
export function activateAndPaste(providerId, text) {
|
|
225
|
+
const recipe = IDE_RECIPES[providerId];
|
|
226
|
+
if (!recipe) {
|
|
227
|
+
log.warn(`No automation recipe for '${providerId}'`);
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Step 1: Check process is running
|
|
232
|
+
const processName = IS_MAC ? recipe.processName.mac : recipe.processName.win;
|
|
233
|
+
if (!isProcessRunning(processName)) {
|
|
234
|
+
log.warn(`${recipe.appName[IS_MAC ? 'mac' : 'win']} is not running`);
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Step 2: Copy to clipboard (handled by caller, but ensure it's done)
|
|
239
|
+
copyToSystemClipboard(text);
|
|
240
|
+
|
|
241
|
+
// Step 3: Run platform-specific automation
|
|
242
|
+
log.info(`Auto-pasting into ${recipe.appName[IS_MAC ? 'mac' : 'win']}...`);
|
|
243
|
+
|
|
244
|
+
if (IS_MAC) {
|
|
245
|
+
const script = buildAppleScript(recipe);
|
|
246
|
+
return runAppleScript(script);
|
|
247
|
+
} else if (IS_WIN) {
|
|
248
|
+
const script = buildPowerShell(recipe);
|
|
249
|
+
return runPowerShell(script);
|
|
250
|
+
} else {
|
|
251
|
+
log.warn('Desktop automation not supported on this platform');
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Copy text to system clipboard (cross-platform)
|
|
258
|
+
*/
|
|
259
|
+
function copyToSystemClipboard(text) {
|
|
260
|
+
try {
|
|
261
|
+
if (IS_MAC) {
|
|
262
|
+
execSync('pbcopy', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
263
|
+
} else if (IS_WIN) {
|
|
264
|
+
execSync('clip', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
265
|
+
} else {
|
|
266
|
+
try {
|
|
267
|
+
execSync('xclip -selection clipboard', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
268
|
+
} catch {
|
|
269
|
+
execSync('xsel --clipboard --input', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
} catch {
|
|
273
|
+
// Clipboard failure is non-fatal — user can copy file manually
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export const automator = {
|
|
278
|
+
isProcessRunning, hasRecipe, isIDERunning, activateAndPaste,
|
|
279
|
+
};
|