runsignal 0.1.2 → 0.1.3
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/README.md +20 -3
- package/bin/runsignal.js +221 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,9 +19,12 @@ npx runsignal --help
|
|
|
19
19
|
- `runsignal login`
|
|
20
20
|
- `runsignal init`
|
|
21
21
|
- `runsignal wrap -- <command>`
|
|
22
|
-
- `runsignal send --message "..."`
|
|
23
|
-
- `runsignal status <eventId>`
|
|
24
|
-
- `runsignal logout`
|
|
22
|
+
- `runsignal send --message "..."`
|
|
23
|
+
- `runsignal status <eventId>`
|
|
24
|
+
- `runsignal logout`
|
|
25
|
+
- `runsignal hooks install --shell powershell`
|
|
26
|
+
- `runsignal hooks doctor --shell powershell`
|
|
27
|
+
- `runsignal hooks uninstall --shell powershell`
|
|
25
28
|
|
|
26
29
|
## Quickstart
|
|
27
30
|
|
|
@@ -34,4 +37,18 @@ npx runsignal@latest wrap -- claude "refactor auth flow"
|
|
|
34
37
|
## Environment
|
|
35
38
|
|
|
36
39
|
- `RUNSIGNAL_APP_URL` to override app URL (default: `https://runsignal.dev`)
|
|
40
|
+
|
|
41
|
+
## Automatic Wrapping
|
|
42
|
+
|
|
43
|
+
To keep using `claude ...` / `codex ...` normally while routing through RunSignal:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
runsignal hooks install --shell powershell
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then restart terminal and verify:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
runsignal hooks doctor --shell powershell
|
|
53
|
+
```
|
|
37
54
|
|
package/bin/runsignal.js
CHANGED
|
@@ -40,6 +40,8 @@ function toUserMessage(errorCode) {
|
|
|
40
40
|
if (code.includes('message_required')) return 'Message is required. Use --message "..."';
|
|
41
41
|
if (code.includes('event_id_required')) return 'Event id is required. Usage: runsignal status <eventId>';
|
|
42
42
|
if (code.includes('init_failed')) return 'Initialization failed. Check token/provider config and try again.';
|
|
43
|
+
if (code.includes('hooks_shell_not_supported'))
|
|
44
|
+
return 'Hooks shell not supported. Use --shell powershell|bash|zsh|all.';
|
|
43
45
|
return code || 'Unknown error';
|
|
44
46
|
}
|
|
45
47
|
|
|
@@ -98,6 +100,162 @@ function sleep(ms) {
|
|
|
98
100
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
const HOOK_START = '# >>> RunSignal hooks start >>>';
|
|
104
|
+
const HOOK_END = '# <<< RunSignal hooks end <<<';
|
|
105
|
+
|
|
106
|
+
function stripHookBlock(content) {
|
|
107
|
+
const start = content.indexOf(HOOK_START);
|
|
108
|
+
const end = content.indexOf(HOOK_END);
|
|
109
|
+
if (start === -1 || end === -1 || end < start) {
|
|
110
|
+
return content;
|
|
111
|
+
}
|
|
112
|
+
const before = content.slice(0, start).trimEnd();
|
|
113
|
+
const after = content.slice(end + HOOK_END.length).trimStart();
|
|
114
|
+
if (!before && !after) return '';
|
|
115
|
+
if (!before) return `${after}\n`;
|
|
116
|
+
if (!after) return `${before}\n`;
|
|
117
|
+
return `${before}\n\n${after}\n`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function buildHookBlock(shellKind) {
|
|
121
|
+
if (shellKind === 'powershell') {
|
|
122
|
+
return `${HOOK_START}
|
|
123
|
+
function claude { runsignal wrap -- claude @args }
|
|
124
|
+
function codex { runsignal wrap -- codex @args }
|
|
125
|
+
function cursor { runsignal wrap -- cursor @args }
|
|
126
|
+
function gemini { runsignal wrap -- gemini @args }
|
|
127
|
+
function aider { runsignal wrap -- aider @args }
|
|
128
|
+
${HOOK_END}
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return `${HOOK_START}
|
|
133
|
+
claude() { runsignal wrap -- claude "$@"; }
|
|
134
|
+
codex() { runsignal wrap -- codex "$@"; }
|
|
135
|
+
cursor() { runsignal wrap -- cursor "$@"; }
|
|
136
|
+
gemini() { runsignal wrap -- gemini "$@"; }
|
|
137
|
+
aider() { runsignal wrap -- aider "$@"; }
|
|
138
|
+
${HOOK_END}
|
|
139
|
+
`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function resolveHookTargets(shellOption) {
|
|
143
|
+
const shell = String(shellOption || 'auto').toLowerCase();
|
|
144
|
+
const home = os.homedir();
|
|
145
|
+
|
|
146
|
+
if (process.platform === 'win32') {
|
|
147
|
+
const powershellTargets = [
|
|
148
|
+
{
|
|
149
|
+
path: path.join(home, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1'),
|
|
150
|
+
kind: 'powershell'
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
path: path.join(home, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1'),
|
|
154
|
+
kind: 'powershell'
|
|
155
|
+
}
|
|
156
|
+
];
|
|
157
|
+
if (shell === 'auto' || shell === 'powershell' || shell === 'all') {
|
|
158
|
+
return powershellTargets;
|
|
159
|
+
}
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (shell === 'zsh') return [{path: path.join(home, '.zshrc'), kind: 'posix'}];
|
|
164
|
+
if (shell === 'bash') return [{path: path.join(home, '.bashrc'), kind: 'posix'}];
|
|
165
|
+
if (shell === 'all') {
|
|
166
|
+
return [
|
|
167
|
+
{path: path.join(home, '.bashrc'), kind: 'posix'},
|
|
168
|
+
{path: path.join(home, '.zshrc'), kind: 'posix'}
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
if (shell === 'auto') {
|
|
172
|
+
const shellEnv = String(process.env.SHELL || '').toLowerCase();
|
|
173
|
+
if (shellEnv.includes('zsh')) return [{path: path.join(home, '.zshrc'), kind: 'posix'}];
|
|
174
|
+
return [{path: path.join(home, '.bashrc'), kind: 'posix'}];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function readTextFile(filePath) {
|
|
181
|
+
try {
|
|
182
|
+
return await fs.readFile(filePath, 'utf8');
|
|
183
|
+
} catch {
|
|
184
|
+
return '';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function runHooksInstall(options) {
|
|
189
|
+
const targets = resolveHookTargets(options.shell);
|
|
190
|
+
if (!targets.length) {
|
|
191
|
+
throw new Error('hooks_shell_not_supported');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (const target of targets) {
|
|
195
|
+
const existing = await readTextFile(target.path);
|
|
196
|
+
const cleaned = stripHookBlock(existing).trimEnd();
|
|
197
|
+
const block = buildHookBlock(target.kind);
|
|
198
|
+
const next = cleaned ? `${cleaned}\n\n${block}` : block;
|
|
199
|
+
await fs.mkdir(path.dirname(target.path), {recursive: true});
|
|
200
|
+
await fs.writeFile(target.path, next, 'utf8');
|
|
201
|
+
console.log(`Hooks installed: ${target.path}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
console.log('Restart your terminal to apply hooks.');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function runHooksUninstall(options) {
|
|
208
|
+
const targets = resolveHookTargets(options.shell);
|
|
209
|
+
if (!targets.length) {
|
|
210
|
+
throw new Error('hooks_shell_not_supported');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (const target of targets) {
|
|
214
|
+
const existing = await readTextFile(target.path);
|
|
215
|
+
if (!existing) {
|
|
216
|
+
console.log(`Hooks not present: ${target.path}`);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const next = stripHookBlock(existing);
|
|
220
|
+
if (next === existing) {
|
|
221
|
+
console.log(`Hooks not present: ${target.path}`);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
await fs.writeFile(target.path, next, 'utf8');
|
|
225
|
+
console.log(`Hooks removed: ${target.path}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function runHooksDoctor(options) {
|
|
230
|
+
const targets = resolveHookTargets(options.shell);
|
|
231
|
+
if (!targets.length) {
|
|
232
|
+
throw new Error('hooks_shell_not_supported');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let healthy = true;
|
|
236
|
+
for (const target of targets) {
|
|
237
|
+
const existing = await readTextFile(target.path);
|
|
238
|
+
const hasHooks = existing.includes(HOOK_START) && existing.includes(HOOK_END);
|
|
239
|
+
if (!hasHooks) {
|
|
240
|
+
healthy = false;
|
|
241
|
+
}
|
|
242
|
+
console.log(
|
|
243
|
+
JSON.stringify(
|
|
244
|
+
{
|
|
245
|
+
path: target.path,
|
|
246
|
+
installed: hasHooks
|
|
247
|
+
},
|
|
248
|
+
null,
|
|
249
|
+
2
|
|
250
|
+
)
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!healthy) {
|
|
255
|
+
process.exitCode = 1;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
101
259
|
function getMachineName() {
|
|
102
260
|
return process.env.COMPUTERNAME || os.hostname() || 'machine';
|
|
103
261
|
}
|
|
@@ -171,6 +329,18 @@ function extractInterestingLine(chunk) {
|
|
|
171
329
|
return null;
|
|
172
330
|
}
|
|
173
331
|
|
|
332
|
+
function quoteForShell(arg) {
|
|
333
|
+
const value = String(arg ?? '');
|
|
334
|
+
if (value.length === 0) {
|
|
335
|
+
return '""';
|
|
336
|
+
}
|
|
337
|
+
if (!/[\s"]/g.test(value)) {
|
|
338
|
+
return value;
|
|
339
|
+
}
|
|
340
|
+
const escaped = value.replace(/(["\\$`])/g, '\\$1');
|
|
341
|
+
return `"${escaped}"`;
|
|
342
|
+
}
|
|
343
|
+
|
|
174
344
|
async function runLogin(options) {
|
|
175
345
|
const config = await readConfig();
|
|
176
346
|
const baseUrl = String(options.appUrl || config.app_url || DEFAULT_APP_URL).replace(/\/$/, '');
|
|
@@ -325,9 +495,14 @@ async function runWrap(commandParts) {
|
|
|
325
495
|
agent_label: agentLabel
|
|
326
496
|
};
|
|
327
497
|
|
|
328
|
-
const commandString =
|
|
498
|
+
const commandString =
|
|
499
|
+
commandParts.length === 1
|
|
500
|
+
? String(commandParts[0])
|
|
501
|
+
: commandParts.map((part) => quoteForShell(part)).join(' ');
|
|
502
|
+
const interactivePassthrough =
|
|
503
|
+
commandParts.length === 1 && !/[\s"'`]/.test(String(commandParts[0]));
|
|
329
504
|
const child = spawn(commandString, {
|
|
330
|
-
stdio: ['inherit', 'pipe', 'pipe'],
|
|
505
|
+
stdio: interactivePassthrough ? 'inherit' : ['inherit', 'pipe', 'pipe'],
|
|
331
506
|
shell: true,
|
|
332
507
|
env: process.env
|
|
333
508
|
});
|
|
@@ -335,6 +510,12 @@ async function runWrap(commandParts) {
|
|
|
335
510
|
let sawApproval = false;
|
|
336
511
|
let sawQuestion = false;
|
|
337
512
|
let lastInterestingLine = '';
|
|
513
|
+
const pendingTasks = new Set();
|
|
514
|
+
|
|
515
|
+
const queueTask = (promise) => {
|
|
516
|
+
pendingTasks.add(promise);
|
|
517
|
+
promise.finally(() => pendingTasks.delete(promise));
|
|
518
|
+
};
|
|
338
519
|
|
|
339
520
|
const handleChunk = async (chunkRaw) => {
|
|
340
521
|
const chunk = chunkRaw.toString('utf8');
|
|
@@ -412,18 +593,21 @@ async function runWrap(commandParts) {
|
|
|
412
593
|
}
|
|
413
594
|
};
|
|
414
595
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
596
|
+
if (!interactivePassthrough) {
|
|
597
|
+
child.stdout.on('data', (chunk) => {
|
|
598
|
+
process.stdout.write(chunk);
|
|
599
|
+
queueTask(handleChunk(chunk));
|
|
600
|
+
});
|
|
601
|
+
child.stderr.on('data', (chunk) => {
|
|
602
|
+
process.stderr.write(chunk);
|
|
603
|
+
queueTask(handleChunk(chunk));
|
|
604
|
+
});
|
|
605
|
+
}
|
|
423
606
|
|
|
424
607
|
const exitCode = await new Promise((resolve) => {
|
|
425
608
|
child.on('close', (code) => resolve(code ?? 1));
|
|
426
609
|
});
|
|
610
|
+
await Promise.allSettled(Array.from(pendingTasks));
|
|
427
611
|
|
|
428
612
|
const completionMessage =
|
|
429
613
|
exitCode === 0
|
|
@@ -449,7 +633,9 @@ async function runWrap(commandParts) {
|
|
|
449
633
|
// no-op
|
|
450
634
|
}
|
|
451
635
|
|
|
452
|
-
process.
|
|
636
|
+
// Avoid hard process exit on Windows: let Node close handles cleanly.
|
|
637
|
+
process.exitCode = exitCode;
|
|
638
|
+
return;
|
|
453
639
|
}
|
|
454
640
|
|
|
455
641
|
async function runTest() {
|
|
@@ -609,7 +795,7 @@ async function runLogout() {
|
|
|
609
795
|
|
|
610
796
|
const program = new Command();
|
|
611
797
|
|
|
612
|
-
program.name('runsignal').description('RunSignal CLI').version('0.1.
|
|
798
|
+
program.name('runsignal').description('RunSignal CLI').version('0.1.3');
|
|
613
799
|
|
|
614
800
|
program
|
|
615
801
|
.command('login')
|
|
@@ -666,6 +852,29 @@ program.command('logout').action(async () => {
|
|
|
666
852
|
await runLogout();
|
|
667
853
|
});
|
|
668
854
|
|
|
855
|
+
const hooks = program.command('hooks').description('Manage shell hooks for automatic wrapping');
|
|
856
|
+
|
|
857
|
+
hooks
|
|
858
|
+
.command('install')
|
|
859
|
+
.option('--shell <shell>', 'auto|powershell|bash|zsh|all', 'auto')
|
|
860
|
+
.action(async (options) => {
|
|
861
|
+
await runHooksInstall(options);
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
hooks
|
|
865
|
+
.command('uninstall')
|
|
866
|
+
.option('--shell <shell>', 'auto|powershell|bash|zsh|all', 'auto')
|
|
867
|
+
.action(async (options) => {
|
|
868
|
+
await runHooksUninstall(options);
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
hooks
|
|
872
|
+
.command('doctor')
|
|
873
|
+
.option('--shell <shell>', 'auto|powershell|bash|zsh|all', 'auto')
|
|
874
|
+
.action(async (options) => {
|
|
875
|
+
await runHooksDoctor(options);
|
|
876
|
+
});
|
|
877
|
+
|
|
669
878
|
program.parseAsync(process.argv).catch((error) => {
|
|
670
879
|
const message = error instanceof Error ? error.message : String(error);
|
|
671
880
|
console.error(`RunSignal error: ${toUserMessage(message)}`);
|