runsignal 0.1.5 → 0.1.7
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/bin/runsignal.js +204 -49
- package/package.json +1 -1
package/bin/runsignal.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import {Command} from 'commander';
|
|
4
4
|
import {spawn, execSync} from 'node:child_process';
|
|
5
5
|
import {promises as fs} from 'node:fs';
|
|
6
|
+
import {existsSync} from 'node:fs';
|
|
6
7
|
import os from 'node:os';
|
|
7
8
|
import path from 'node:path';
|
|
8
9
|
import {
|
|
@@ -17,6 +18,20 @@ const DEFAULT_APP_URL = process.env.RUNSIGNAL_APP_URL || 'https://runsignal.dev'
|
|
|
17
18
|
const CONFIG_DIR = path.join(os.homedir(), '.runsignal');
|
|
18
19
|
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
19
20
|
|
|
21
|
+
function extractErrorDetail(code, marker) {
|
|
22
|
+
const index = code.indexOf(marker);
|
|
23
|
+
if (index === -1) {
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const detail = code.slice(index + marker.length).trim();
|
|
28
|
+
if (!detail) {
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return detail.slice(0, 180);
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
function toUserMessage(errorCode) {
|
|
21
36
|
const code = String(errorCode || '');
|
|
22
37
|
if (code.includes('not_logged_in')) return 'You are not logged in. Run: runsignal login';
|
|
@@ -31,8 +46,13 @@ function toUserMessage(errorCode) {
|
|
|
31
46
|
if (code.includes('login_timeout')) return 'Login timed out. Retry runsignal login and validate quickly.';
|
|
32
47
|
if (code.includes('login_poll_failed'))
|
|
33
48
|
return 'Login polling failed. Check APP URL and server availability.';
|
|
34
|
-
if (code.includes('send_event_failed'))
|
|
49
|
+
if (code.includes('send_event_failed')) {
|
|
50
|
+
const detail = extractErrorDetail(code, 'send_event_failed:');
|
|
51
|
+
if (detail) {
|
|
52
|
+
return `Event send failed (${detail}). Check machine key, network and server logs.`;
|
|
53
|
+
}
|
|
35
54
|
return 'Event send failed. Check machine key, network and server logs.';
|
|
55
|
+
}
|
|
36
56
|
if (code.includes('status_failed')) return 'Status check failed. Verify event id and machine ownership.';
|
|
37
57
|
if (code.includes('event_type_invalid'))
|
|
38
58
|
return 'Invalid event type. Use approval_required, task_completed, error, quota_warning, question.';
|
|
@@ -100,6 +120,39 @@ function sleep(ms) {
|
|
|
100
120
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
101
121
|
}
|
|
102
122
|
|
|
123
|
+
function tryOpenBrowser(url) {
|
|
124
|
+
if (!url) return false;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
if (process.platform === 'win32') {
|
|
128
|
+
const child = spawn('cmd', ['/c', 'start', '', url], {
|
|
129
|
+
detached: true,
|
|
130
|
+
stdio: 'ignore'
|
|
131
|
+
});
|
|
132
|
+
child.unref();
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (process.platform === 'darwin') {
|
|
137
|
+
const child = spawn('open', [url], {
|
|
138
|
+
detached: true,
|
|
139
|
+
stdio: 'ignore'
|
|
140
|
+
});
|
|
141
|
+
child.unref();
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const child = spawn('xdg-open', [url], {
|
|
146
|
+
detached: true,
|
|
147
|
+
stdio: 'ignore'
|
|
148
|
+
});
|
|
149
|
+
child.unref();
|
|
150
|
+
return true;
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
103
156
|
const HOOK_START = '# >>> RunSignal hooks start >>>';
|
|
104
157
|
const HOOK_END = '# <<< RunSignal hooks end <<<';
|
|
105
158
|
|
|
@@ -122,43 +175,44 @@ function buildHookBlock(shellKind) {
|
|
|
122
175
|
return `${HOOK_START}
|
|
123
176
|
function RunSignal-Resolve-NativeCommand {
|
|
124
177
|
param([string]$Name)
|
|
125
|
-
$
|
|
126
|
-
if (-not $
|
|
127
|
-
|
|
178
|
+
$candidates = @(Get-Command $Name -CommandType Application -ErrorAction SilentlyContinue)
|
|
179
|
+
if (-not $candidates -or $candidates.Count -eq 0) { return $null }
|
|
180
|
+
$preferred = $candidates | Where-Object { $_.Source -match '\\.exe$' } | Select-Object -First 1
|
|
181
|
+
if ($preferred) { return $preferred.Source }
|
|
182
|
+
$launcher = $candidates | Where-Object { $_.Source -match '\\.(cmd|bat)$' } | Select-Object -First 1
|
|
183
|
+
if ($launcher) { return $launcher.Source }
|
|
184
|
+
$secondary = $candidates | Where-Object { $_.Source -match '\\.[A-Za-z0-9]+$' } | Select-Object -First 1
|
|
185
|
+
if ($secondary) { return $secondary.Source }
|
|
186
|
+
return $candidates[0].Source
|
|
128
187
|
}
|
|
129
188
|
function claude {
|
|
130
189
|
$native = RunSignal-Resolve-NativeCommand "claude"
|
|
131
190
|
if (-not $native) {
|
|
132
|
-
$
|
|
133
|
-
$fallback = Join-Path $
|
|
191
|
+
$userHome = [Environment]::GetFolderPath("UserProfile")
|
|
192
|
+
$fallback = Join-Path $userHome ".local\\bin\\claude.exe"
|
|
134
193
|
if (Test-Path $fallback) { $native = $fallback }
|
|
135
194
|
}
|
|
136
195
|
if (-not $native) { Write-Error "RunSignal hook: native claude executable not found. Add your native installer path to PATH (example: %USERPROFILE%\\.local\\bin)."; return }
|
|
137
|
-
if ($args.Count -eq 0) { & $native; return }
|
|
138
196
|
runsignal wrap -- $native @args
|
|
139
197
|
}
|
|
140
198
|
function codex {
|
|
141
199
|
$native = RunSignal-Resolve-NativeCommand "codex"
|
|
142
200
|
if (-not $native) { Write-Error "RunSignal hook: native codex executable not found."; return }
|
|
143
|
-
if ($args.Count -eq 0) { & $native; return }
|
|
144
201
|
runsignal wrap -- $native @args
|
|
145
202
|
}
|
|
146
203
|
function cursor {
|
|
147
204
|
$native = RunSignal-Resolve-NativeCommand "cursor"
|
|
148
205
|
if (-not $native) { Write-Error "RunSignal hook: native cursor executable not found."; return }
|
|
149
|
-
if ($args.Count -eq 0) { & $native; return }
|
|
150
206
|
runsignal wrap -- $native @args
|
|
151
207
|
}
|
|
152
208
|
function gemini {
|
|
153
209
|
$native = RunSignal-Resolve-NativeCommand "gemini"
|
|
154
210
|
if (-not $native) { Write-Error "RunSignal hook: native gemini executable not found."; return }
|
|
155
|
-
if ($args.Count -eq 0) { & $native; return }
|
|
156
211
|
runsignal wrap -- $native @args
|
|
157
212
|
}
|
|
158
213
|
function aider {
|
|
159
214
|
$native = RunSignal-Resolve-NativeCommand "aider"
|
|
160
215
|
if (-not $native) { Write-Error "RunSignal hook: native aider executable not found."; return }
|
|
161
|
-
if ($args.Count -eq 0) { & $native; return }
|
|
162
216
|
runsignal wrap -- $native @args
|
|
163
217
|
}
|
|
164
218
|
${HOOK_END}
|
|
@@ -183,35 +237,30 @@ claude() {
|
|
|
183
237
|
local native
|
|
184
238
|
native="$(runsignal_resolve_native claude)"
|
|
185
239
|
if [ -z "$native" ]; then echo "RunSignal hook: native claude executable not found." >&2; return 127; fi
|
|
186
|
-
if [ "$#" -eq 0 ]; then "$native"; return $?; fi
|
|
187
240
|
runsignal wrap -- "$native" "$@"
|
|
188
241
|
}
|
|
189
242
|
codex() {
|
|
190
243
|
local native
|
|
191
244
|
native="$(runsignal_resolve_native codex)"
|
|
192
245
|
if [ -z "$native" ]; then echo "RunSignal hook: native codex executable not found." >&2; return 127; fi
|
|
193
|
-
if [ "$#" -eq 0 ]; then "$native"; return $?; fi
|
|
194
246
|
runsignal wrap -- "$native" "$@"
|
|
195
247
|
}
|
|
196
248
|
cursor() {
|
|
197
249
|
local native
|
|
198
250
|
native="$(runsignal_resolve_native cursor)"
|
|
199
251
|
if [ -z "$native" ]; then echo "RunSignal hook: native cursor executable not found." >&2; return 127; fi
|
|
200
|
-
if [ "$#" -eq 0 ]; then "$native"; return $?; fi
|
|
201
252
|
runsignal wrap -- "$native" "$@"
|
|
202
253
|
}
|
|
203
254
|
gemini() {
|
|
204
255
|
local native
|
|
205
256
|
native="$(runsignal_resolve_native gemini)"
|
|
206
257
|
if [ -z "$native" ]; then echo "RunSignal hook: native gemini executable not found." >&2; return 127; fi
|
|
207
|
-
if [ "$#" -eq 0 ]; then "$native"; return $?; fi
|
|
208
258
|
runsignal wrap -- "$native" "$@"
|
|
209
259
|
}
|
|
210
260
|
aider() {
|
|
211
261
|
local native
|
|
212
262
|
native="$(runsignal_resolve_native aider)"
|
|
213
263
|
if [ -z "$native" ]; then echo "RunSignal hook: native aider executable not found." >&2; return 127; fi
|
|
214
|
-
if [ "$#" -eq 0 ]; then "$native"; return $?; fi
|
|
215
264
|
runsignal wrap -- "$native" "$@"
|
|
216
265
|
}
|
|
217
266
|
${HOOK_END}
|
|
@@ -435,6 +484,35 @@ function quoteForShell(arg) {
|
|
|
435
484
|
return `"${escaped}"`;
|
|
436
485
|
}
|
|
437
486
|
|
|
487
|
+
function stripWrappingQuotes(value) {
|
|
488
|
+
const raw = String(value || '').trim();
|
|
489
|
+
if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
|
|
490
|
+
return raw.slice(1, -1);
|
|
491
|
+
}
|
|
492
|
+
return raw;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function isWindowsScriptLauncher(filePath) {
|
|
496
|
+
if (process.platform !== 'win32') return false;
|
|
497
|
+
const ext = path.extname(String(filePath || '')).toLowerCase();
|
|
498
|
+
return ext === '.cmd' || ext === '.bat' || ext === '.ps1';
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function normalizeWindowsExecutablePath(filePath) {
|
|
502
|
+
const raw = String(filePath || '');
|
|
503
|
+
if (process.platform !== 'win32') return raw;
|
|
504
|
+
if (!raw) return raw;
|
|
505
|
+
const ext = path.extname(raw).toLowerCase();
|
|
506
|
+
if (ext) return raw;
|
|
507
|
+
const exe = `${raw}.exe`;
|
|
508
|
+
if (existsSync(exe)) return exe;
|
|
509
|
+
const cmd = `${raw}.cmd`;
|
|
510
|
+
if (existsSync(cmd)) return cmd;
|
|
511
|
+
const bat = `${raw}.bat`;
|
|
512
|
+
if (existsSync(bat)) return bat;
|
|
513
|
+
return raw;
|
|
514
|
+
}
|
|
515
|
+
|
|
438
516
|
async function runLogin(options) {
|
|
439
517
|
const config = await readConfig();
|
|
440
518
|
const baseUrl = String(options.appUrl || config.app_url || DEFAULT_APP_URL).replace(/\/$/, '');
|
|
@@ -453,7 +531,13 @@ async function runLogin(options) {
|
|
|
453
531
|
throw new Error('login_payload_invalid');
|
|
454
532
|
}
|
|
455
533
|
|
|
456
|
-
|
|
534
|
+
const verificationUrlWithCode = `${verificationUrl}?code=${encodeURIComponent(String(userCode))}`;
|
|
535
|
+
const opened = tryOpenBrowser(verificationUrlWithCode);
|
|
536
|
+
if (opened) {
|
|
537
|
+
console.log('Browser opened.');
|
|
538
|
+
} else {
|
|
539
|
+
console.log(`Open ${verificationUrlWithCode} and enter code: ${userCode}`);
|
|
540
|
+
}
|
|
457
541
|
|
|
458
542
|
let accessToken = null;
|
|
459
543
|
const deadline = Date.now() + 10 * 60 * 1000;
|
|
@@ -578,7 +662,7 @@ async function runWrap(commandParts) {
|
|
|
578
662
|
}
|
|
579
663
|
|
|
580
664
|
const repo = getRepoFromGit();
|
|
581
|
-
const branch = getBranchFromGit();
|
|
665
|
+
const branch = getBranchFromGit() || undefined;
|
|
582
666
|
const machineName = config.machine_name || getMachineName();
|
|
583
667
|
const agentLabel = inferAgentLabel(commandParts);
|
|
584
668
|
|
|
@@ -593,13 +677,75 @@ async function runWrap(commandParts) {
|
|
|
593
677
|
commandParts.length === 1
|
|
594
678
|
? String(commandParts[0])
|
|
595
679
|
: commandParts.map((part) => quoteForShell(part)).join(' ');
|
|
680
|
+
const singleCommand = String(commandParts[0] || '');
|
|
681
|
+
const singleExecutable = normalizeWindowsExecutablePath(stripWrappingQuotes(singleCommand));
|
|
596
682
|
const interactivePassthrough =
|
|
597
|
-
commandParts.length === 1 &&
|
|
598
|
-
const child =
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
683
|
+
commandParts.length === 1 && (existsSync(singleExecutable) || !/\s/.test(singleExecutable));
|
|
684
|
+
const child = interactivePassthrough
|
|
685
|
+
? isWindowsScriptLauncher(singleExecutable)
|
|
686
|
+
? spawn(singleExecutable, [], {
|
|
687
|
+
stdio: 'inherit',
|
|
688
|
+
shell: true,
|
|
689
|
+
env: process.env
|
|
690
|
+
})
|
|
691
|
+
: spawn(singleExecutable, [], {
|
|
692
|
+
stdio: 'inherit',
|
|
693
|
+
shell: false,
|
|
694
|
+
env: process.env
|
|
695
|
+
})
|
|
696
|
+
: spawn(commandString, {
|
|
697
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
698
|
+
shell: true,
|
|
699
|
+
env: process.env
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
const startedAt = Date.now();
|
|
703
|
+
const sessionTarget = interactivePassthrough ? singleExecutable : commandString;
|
|
704
|
+
try {
|
|
705
|
+
await sendEvent({
|
|
706
|
+
apiFetch,
|
|
707
|
+
baseUrl,
|
|
708
|
+
machineApiKey,
|
|
709
|
+
event: {
|
|
710
|
+
message: `Session started: ${sessionTarget}`,
|
|
711
|
+
event_type: 'session_started',
|
|
712
|
+
repo,
|
|
713
|
+
branch,
|
|
714
|
+
severity: 'info',
|
|
715
|
+
context,
|
|
716
|
+
agent: agentLabel
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
} catch {
|
|
720
|
+
// non-blocking
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
let heartbeatInterval = null;
|
|
724
|
+
if (interactivePassthrough) {
|
|
725
|
+
const heartbeatMin = Number.parseInt(String(process.env.RUNSIGNAL_HEARTBEAT_MIN ?? '0'), 10);
|
|
726
|
+
if (Number.isFinite(heartbeatMin) && heartbeatMin > 0) {
|
|
727
|
+
heartbeatInterval = setInterval(() => {
|
|
728
|
+
const elapsedMin = Math.max(1, Math.floor((Date.now() - startedAt) / 60_000));
|
|
729
|
+
sendEvent({
|
|
730
|
+
apiFetch,
|
|
731
|
+
baseUrl,
|
|
732
|
+
machineApiKey,
|
|
733
|
+
event: {
|
|
734
|
+
message: `Session running (${elapsedMin} min): ${singleExecutable}`,
|
|
735
|
+
event_type: 'session_running',
|
|
736
|
+
repo,
|
|
737
|
+
branch,
|
|
738
|
+
severity: 'info',
|
|
739
|
+
context,
|
|
740
|
+
agent: agentLabel
|
|
741
|
+
}
|
|
742
|
+
}).catch(() => {
|
|
743
|
+
// non-blocking
|
|
744
|
+
});
|
|
745
|
+
}, heartbeatMin * 60_000);
|
|
746
|
+
heartbeatInterval.unref?.();
|
|
747
|
+
}
|
|
748
|
+
}
|
|
603
749
|
|
|
604
750
|
let sawApproval = false;
|
|
605
751
|
let sawQuestion = false;
|
|
@@ -699,32 +845,38 @@ async function runWrap(commandParts) {
|
|
|
699
845
|
}
|
|
700
846
|
|
|
701
847
|
const exitCode = await new Promise((resolve) => {
|
|
848
|
+
child.on('error', () => resolve(1));
|
|
702
849
|
child.on('close', (code) => resolve(code ?? 1));
|
|
703
850
|
});
|
|
851
|
+
if (heartbeatInterval) {
|
|
852
|
+
clearInterval(heartbeatInterval);
|
|
853
|
+
}
|
|
704
854
|
await Promise.allSettled(Array.from(pendingTasks));
|
|
705
855
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
856
|
+
if (!interactivePassthrough || exitCode !== 0) {
|
|
857
|
+
const completionMessage =
|
|
858
|
+
exitCode === 0
|
|
859
|
+
? `Wrapped command completed: ${commandParts.join(' ')}`
|
|
860
|
+
: `Wrapped command failed (code ${exitCode}): ${lastInterestingLine || commandParts.join(' ')}`;
|
|
710
861
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
862
|
+
try {
|
|
863
|
+
await sendEvent({
|
|
864
|
+
apiFetch,
|
|
865
|
+
baseUrl,
|
|
866
|
+
machineApiKey,
|
|
867
|
+
event: {
|
|
868
|
+
message: completionMessage.slice(0, 1000),
|
|
869
|
+
event_type: exitCode === 0 ? 'task_completed' : 'error',
|
|
870
|
+
repo,
|
|
871
|
+
branch,
|
|
872
|
+
severity: exitCode === 0 ? 'info' : 'critical',
|
|
873
|
+
context,
|
|
874
|
+
agent: agentLabel
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
} catch {
|
|
878
|
+
// no-op
|
|
879
|
+
}
|
|
728
880
|
}
|
|
729
881
|
|
|
730
882
|
// Avoid hard process exit on Windows: let Node close handles cleanly.
|
|
@@ -742,7 +894,7 @@ async function runTest() {
|
|
|
742
894
|
}
|
|
743
895
|
|
|
744
896
|
const repo = getRepoFromGit();
|
|
745
|
-
const branch = getBranchFromGit();
|
|
897
|
+
const branch = getBranchFromGit() || undefined;
|
|
746
898
|
|
|
747
899
|
const result = await sendEvent({
|
|
748
900
|
apiFetch,
|
|
@@ -774,7 +926,9 @@ function normalizeEventType(value) {
|
|
|
774
926
|
raw === 'task_completed' ||
|
|
775
927
|
raw === 'error' ||
|
|
776
928
|
raw === 'quota_warning' ||
|
|
777
|
-
raw === 'question'
|
|
929
|
+
raw === 'question' ||
|
|
930
|
+
raw === 'session_started' ||
|
|
931
|
+
raw === 'session_running'
|
|
778
932
|
) {
|
|
779
933
|
return raw;
|
|
780
934
|
}
|
|
@@ -811,7 +965,8 @@ async function runSend(options) {
|
|
|
811
965
|
}
|
|
812
966
|
|
|
813
967
|
const repo = options.repo ? String(options.repo).trim() : getRepoFromGit();
|
|
814
|
-
const
|
|
968
|
+
const branchRaw = options.branch ? String(options.branch).trim() : getBranchFromGit();
|
|
969
|
+
const branch = branchRaw || undefined;
|
|
815
970
|
const agent = options.agent ? String(options.agent).trim() : 'manual-cli';
|
|
816
971
|
|
|
817
972
|
const result = await sendEvent({
|
|
@@ -944,7 +1099,7 @@ async function runSetup(options) {
|
|
|
944
1099
|
|
|
945
1100
|
const program = new Command();
|
|
946
1101
|
|
|
947
|
-
program.name('runsignal').description('RunSignal CLI').version('0.1.
|
|
1102
|
+
program.name('runsignal').description('RunSignal CLI').version('0.1.6');
|
|
948
1103
|
|
|
949
1104
|
program
|
|
950
1105
|
.command('setup')
|