@fnclaude/cli 1.1.0 → 2.0.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/bin/fnc.js +34 -79
- package/package.json +6 -9
- package/share/fnclaude/templates/handoff.template.md +11 -0
- package/src/argv/classify.ts +48 -0
- package/src/argv/expand.ts +51 -0
- package/src/argv/intake.ts +52 -0
- package/src/argv/magic.ts +103 -0
- package/src/argv/parse.ts +213 -0
- package/src/argv/preserve-args.ts +333 -0
- package/src/argv/sentinel.ts +41 -0
- package/src/argv/short-flags.ts +152 -0
- package/src/config/load.ts +116 -0
- package/src/handoff/awaiter.ts +140 -0
- package/src/handoff/clean-env.ts +45 -0
- package/src/handoff/kill-and-exec.ts +110 -0
- package/src/handoff/spawn-launcher.ts +185 -0
- package/src/handoff/summary-file.ts +86 -0
- package/src/handoff/trigger.ts +90 -0
- package/src/help-version.ts +151 -0
- package/src/launch/compose-env.ts +34 -0
- package/src/launch/cross-cwd-parse.ts +69 -0
- package/src/launch/cross-cwd-relaunch.ts +95 -0
- package/src/launch/find-claude.ts +52 -0
- package/src/launch/live-permission-reader.ts +133 -0
- package/src/launch/ring-buffer.ts +92 -0
- package/src/main.ts +580 -437
- package/src/mcp/dispatch.ts +240 -0
- package/src/mcp/handlers/clipboard-backends.ts +176 -0
- package/src/mcp/handlers/clipboard.ts +62 -0
- package/src/mcp/handlers/restart.ts +156 -0
- package/src/mcp/handlers/spawn.ts +219 -0
- package/src/mcp/handlers/switch.ts +272 -0
- package/src/mcp/inject-config.ts +59 -0
- package/src/mcp/jsonrpc-server.ts +154 -0
- package/src/mcp/listener.ts +141 -0
- package/src/mcp/parent-dispatch.ts +154 -0
- package/src/mcp/socket-path.ts +48 -0
- package/src/mcp/wire.ts +181 -0
- package/src/name/auto-name.ts +162 -0
- package/src/name/llm-prompt.ts +14 -0
- package/src/name/sanitize.ts +57 -0
- package/src/name/sdk-llm.ts +42 -0
- package/src/noop/seed.ts +63 -0
- package/src/noop/template-source.ts +62 -0
- package/src/path/ensure-cwd.ts +95 -0
- package/src/path/resolve.ts +58 -0
- package/src/prompts/dir.ts +61 -0
- package/src/prompts/load.ts +100 -0
- package/src/prompts/select.ts +43 -0
- package/src/repo/clone-exec.ts +37 -0
- package/src/repo/clone.ts +45 -0
- package/src/repo/gh-runner.ts +68 -0
- package/src/repo/host-aliases.ts +58 -0
- package/src/repo/owner-lookup.ts +71 -0
- package/src/repo/ref.ts +146 -0
- package/src/repo/repo-settings.ts +99 -0
- package/src/repo/resolve-input.ts +179 -0
- package/src/repo/template.ts +92 -0
- package/src/warnings/buffer.ts +39 -0
- package/src/worktree/auto-tmux.ts +45 -0
- package/src/worktree/git-list.ts +73 -0
- package/src/worktree/intercept.ts +150 -0
- package/bin/preflight.js +0 -66
- package/prompts/agent-pitfall.md +0 -1
- package/prompts/noop-router.md +0 -186
- package/prompts/project-switch.md +0 -64
- package/prompts/restart.md +0 -50
- package/prompts/spawn.md +0 -62
- package/src/argParser.ts +0 -367
- package/src/args/preserve.ts +0 -338
- package/src/args.ts +0 -239
- package/src/argv.ts +0 -203
- package/src/autoname.ts +0 -273
- package/src/clipboard.ts +0 -149
- package/src/config.ts +0 -369
- package/src/errors.ts +0 -13
- package/src/handoff.ts +0 -108
- package/src/help.ts +0 -139
- package/src/hostAliases.ts +0 -139
- package/src/index.ts +0 -120
- package/src/mcp/client.ts +0 -645
- package/src/mcp/protocol.ts +0 -445
- package/src/mcp/socketListener.ts +0 -540
- package/src/noop.ts +0 -106
- package/src/passthrough.ts +0 -36
- package/src/paths.ts +0 -55
- package/src/prompts.ts +0 -279
- package/src/pty/unix.ts +0 -429
- package/src/pty/windows.ts +0 -125
- package/src/pty.ts +0 -380
- package/src/repoRef.ts +0 -158
- package/src/repoSettings.ts +0 -144
- package/src/resolver.ts +0 -519
- package/src/sanitize.ts +0 -120
- package/src/sessionState.ts +0 -220
- package/src/silentRelaunch.ts +0 -178
- package/src/spawn.ts +0 -163
- package/src/template.ts +0 -44
- package/src/warnings.ts +0 -34
- package/src/worktree.ts +0 -201
package/src/autoname.ts
DELETED
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
// Port of src/autoname.go from the Go reference implementation.
|
|
2
|
-
//
|
|
3
|
-
// shouldAutoName — predicate for automatic --name injection.
|
|
4
|
-
// generateName — Anthropic API call (or claude CLI fallback) to produce a
|
|
5
|
-
// short kebab-case session label from the initial prompt.
|
|
6
|
-
|
|
7
|
-
import Anthropic from '@anthropic-ai/sdk';
|
|
8
|
-
import type { NameConfig } from './config.js';
|
|
9
|
-
|
|
10
|
-
// ── predicates ──────────────────────────────────────────────────────────────
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* shouldAutoName returns true when the passthrough slice meets all conditions
|
|
14
|
-
* for automatic --name injection:
|
|
15
|
-
*
|
|
16
|
-
* - contains "--" followed by at least one non-empty token
|
|
17
|
-
* - does NOT already contain --name / -n / --name=* / -n=*
|
|
18
|
-
* - does NOT contain -p / --print
|
|
19
|
-
* - does NOT contain -r / --resume / -r=* / --resume=*
|
|
20
|
-
* - does NOT contain -c / --continue
|
|
21
|
-
* - does NOT contain --from-pr / --from-pr=* / -P / -P=*
|
|
22
|
-
*/
|
|
23
|
-
export function shouldAutoName(passthrough: readonly string[]): boolean {
|
|
24
|
-
// Find "--" and verify at least one non-empty token follows.
|
|
25
|
-
const sepIdx = passthrough.indexOf('--');
|
|
26
|
-
if (sepIdx < 0) return false;
|
|
27
|
-
|
|
28
|
-
const hasPrompt = passthrough.slice(sepIdx + 1).some((t) => t !== '');
|
|
29
|
-
if (!hasPrompt) return false;
|
|
30
|
-
|
|
31
|
-
// Check for disqualifying tokens.
|
|
32
|
-
for (const t of passthrough) {
|
|
33
|
-
if (
|
|
34
|
-
t === '--name' ||
|
|
35
|
-
t === '-n' ||
|
|
36
|
-
t.startsWith('--name=') ||
|
|
37
|
-
t.startsWith('-n=')
|
|
38
|
-
)
|
|
39
|
-
return false;
|
|
40
|
-
if (t === '-p' || t === '--print') return false;
|
|
41
|
-
if (
|
|
42
|
-
t === '-r' ||
|
|
43
|
-
t === '--resume' ||
|
|
44
|
-
t.startsWith('-r=') ||
|
|
45
|
-
t.startsWith('--resume=')
|
|
46
|
-
)
|
|
47
|
-
return false;
|
|
48
|
-
if (t === '-c' || t === '--continue') return false;
|
|
49
|
-
if (
|
|
50
|
-
t === '--from-pr' ||
|
|
51
|
-
t.startsWith('--from-pr=') ||
|
|
52
|
-
t === '-P' ||
|
|
53
|
-
t.startsWith('-P=')
|
|
54
|
-
)
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* extractPrompt returns the first non-empty token after "--" in passthrough.
|
|
62
|
-
* Returns undefined if not found.
|
|
63
|
-
*/
|
|
64
|
-
export function extractPrompt(passthrough: readonly string[]): string | undefined {
|
|
65
|
-
const sepIdx = passthrough.indexOf('--');
|
|
66
|
-
if (sepIdx < 0) return undefined;
|
|
67
|
-
for (const t of passthrough.slice(sepIdx + 1)) {
|
|
68
|
-
if (t !== '') return t;
|
|
69
|
-
}
|
|
70
|
-
return undefined;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ── stop-words + heuristic fallback ────────────────────────────────────────
|
|
74
|
-
|
|
75
|
-
const STOP_WORDS = new Set([
|
|
76
|
-
'a',
|
|
77
|
-
'an',
|
|
78
|
-
'the',
|
|
79
|
-
'is',
|
|
80
|
-
'are',
|
|
81
|
-
'was',
|
|
82
|
-
'were',
|
|
83
|
-
'do',
|
|
84
|
-
'does',
|
|
85
|
-
'did',
|
|
86
|
-
'of',
|
|
87
|
-
'for',
|
|
88
|
-
'to',
|
|
89
|
-
'in',
|
|
90
|
-
'on',
|
|
91
|
-
'at',
|
|
92
|
-
'with',
|
|
93
|
-
'this',
|
|
94
|
-
'that',
|
|
95
|
-
'please',
|
|
96
|
-
'can',
|
|
97
|
-
'could',
|
|
98
|
-
'would',
|
|
99
|
-
'should',
|
|
100
|
-
]);
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* heuristicName derives a session name from a prompt without any LLM call.
|
|
104
|
-
* Takes up to 3 non-stop-word, alphanumeric tokens joined with "-".
|
|
105
|
-
*/
|
|
106
|
-
export function heuristicName(prompt: string): string {
|
|
107
|
-
const words = prompt.toLowerCase().split(/\s+/);
|
|
108
|
-
const kept: string[] = [];
|
|
109
|
-
for (const w of words) {
|
|
110
|
-
if (STOP_WORDS.has(w)) continue;
|
|
111
|
-
const clean = w.replace(/[^a-z0-9]/g, '');
|
|
112
|
-
if (clean !== '') kept.push(clean);
|
|
113
|
-
if (kept.length === 3) break;
|
|
114
|
-
}
|
|
115
|
-
return kept.length === 0 ? 'session' : kept.join('-');
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// ── slug sanitization ───────────────────────────────────────────────────────
|
|
119
|
-
|
|
120
|
-
const RE_NON_SLUG = /[^a-z0-9-]+/g;
|
|
121
|
-
const RE_MULTI_DASH = /-{2,}/g;
|
|
122
|
-
const RE_WHITESPACE = /\s+/g;
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* sanitizeSlug cleans raw LLM output into a valid kebab slug (up to 3 dash-
|
|
126
|
-
* separated segments). Returns undefined when nothing survives sanitization.
|
|
127
|
-
*/
|
|
128
|
-
export function sanitizeSlug(raw: string): string | undefined {
|
|
129
|
-
let s = raw.trim().toLowerCase();
|
|
130
|
-
s = s.replace(RE_WHITESPACE, '-');
|
|
131
|
-
s = s.replace(RE_NON_SLUG, '');
|
|
132
|
-
s = s.replace(RE_MULTI_DASH, '-');
|
|
133
|
-
s = s.replace(/^-+|-+$/g, '');
|
|
134
|
-
// Take first 3 dash-segments.
|
|
135
|
-
const parts = s.split('-');
|
|
136
|
-
s = parts.slice(0, 3).join('-');
|
|
137
|
-
// Trim again in case joining re-introduced edge dashes.
|
|
138
|
-
s = s.replace(/^-+|-+$/g, '');
|
|
139
|
-
return s !== '' ? s : undefined;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// ── LLM client abstraction ──────────────────────────────────────────────────
|
|
143
|
-
|
|
144
|
-
export const NAME_SYSTEM_PROMPT =
|
|
145
|
-
"Generate a 1-3 word lowercase hyphen-separated label for this user's request. " +
|
|
146
|
-
'Output ONLY the label — no punctuation, no quotes, no explanation, no leading ' +
|
|
147
|
-
"'Label:'. Examples: 'fix-login-bug', 'add-dark-mode', 'refactor-auth'.";
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* LlmClientFn is the injectable seam for the LLM call. Tests can swap in a
|
|
151
|
-
* fake without touching the Anthropic SDK.
|
|
152
|
-
*/
|
|
153
|
-
export type LlmClientFn = (
|
|
154
|
-
model: string,
|
|
155
|
-
prompt: string,
|
|
156
|
-
signal: AbortSignal,
|
|
157
|
-
) => Promise<string>;
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* defaultLlmClient returns an LlmClientFn backed by the real Anthropic API.
|
|
161
|
-
*/
|
|
162
|
-
export function defaultLlmClient(apiKey: string): LlmClientFn {
|
|
163
|
-
return async (model, prompt, signal) => {
|
|
164
|
-
const client = new Anthropic({ apiKey });
|
|
165
|
-
const msg = await client.messages.create(
|
|
166
|
-
{
|
|
167
|
-
model,
|
|
168
|
-
max_tokens: 30,
|
|
169
|
-
system: NAME_SYSTEM_PROMPT,
|
|
170
|
-
messages: [{ role: 'user', content: prompt }],
|
|
171
|
-
},
|
|
172
|
-
{ signal },
|
|
173
|
-
);
|
|
174
|
-
for (const blk of msg.content) {
|
|
175
|
-
if (blk.type === 'text') return blk.text;
|
|
176
|
-
}
|
|
177
|
-
throw new Error('no text block in response');
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* claudeCliFn shells out to `claude -p --model <model> <combined-prompt>`.
|
|
183
|
-
* Used as fallback when ANTHROPIC_API_KEY is absent.
|
|
184
|
-
*/
|
|
185
|
-
export type SpawnFn = (
|
|
186
|
-
cmd: string,
|
|
187
|
-
args: string[],
|
|
188
|
-
signal: AbortSignal,
|
|
189
|
-
) => Promise<string>;
|
|
190
|
-
|
|
191
|
-
// Production spawn implementation using Bun.spawn.
|
|
192
|
-
export const defaultSpawnFn: SpawnFn = async (cmd, args, signal) => {
|
|
193
|
-
const proc = Bun.spawn([cmd, ...args], {
|
|
194
|
-
stdout: 'pipe',
|
|
195
|
-
stderr: 'pipe',
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
// Kill on abort.
|
|
199
|
-
signal.addEventListener('abort', () => {
|
|
200
|
-
try {
|
|
201
|
-
proc.kill();
|
|
202
|
-
} catch {
|
|
203
|
-
// ignore
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
const [stdout] = await Promise.all([
|
|
208
|
-
new Response(proc.stdout).text(),
|
|
209
|
-
proc.exited,
|
|
210
|
-
]);
|
|
211
|
-
|
|
212
|
-
const exitCode = proc.exitCode;
|
|
213
|
-
if (exitCode !== 0) {
|
|
214
|
-
throw new Error(`claude exited with code ${exitCode}`);
|
|
215
|
-
}
|
|
216
|
-
return stdout;
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
export function claudeCliFn(model: string, spawnFn: SpawnFn = defaultSpawnFn): LlmClientFn {
|
|
220
|
-
return async (_model, prompt, signal) => {
|
|
221
|
-
const combined = `${NAME_SYSTEM_PROMPT}\n\nUser request: ${prompt}`;
|
|
222
|
-
return spawnFn('claude', ['-p', '--model', model, combined], signal);
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// ── generateName ────────────────────────────────────────────────────────────
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* generateName produces a session name for the given prompt.
|
|
230
|
-
*
|
|
231
|
-
* llmFn may be omitted; it is selected automatically:
|
|
232
|
-
* - defaultLlmClient when apiKey is non-empty
|
|
233
|
-
* - claudeCliFn otherwise (falls back to the user's existing auth)
|
|
234
|
-
*
|
|
235
|
-
* On any error the function falls back to heuristicName silently.
|
|
236
|
-
*/
|
|
237
|
-
export async function generateName(
|
|
238
|
-
prompt: string | undefined,
|
|
239
|
-
cfg: NameConfig,
|
|
240
|
-
apiKey: string,
|
|
241
|
-
llmFn?: LlmClientFn,
|
|
242
|
-
): Promise<string> {
|
|
243
|
-
const resolved = prompt ?? '';
|
|
244
|
-
let usingCLI = false;
|
|
245
|
-
if (!llmFn) {
|
|
246
|
-
if (apiKey) {
|
|
247
|
-
llmFn = defaultLlmClient(apiKey);
|
|
248
|
-
} else {
|
|
249
|
-
llmFn = claudeCliFn(cfg.model);
|
|
250
|
-
usingCLI = true;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// cfg.timeout is in milliseconds (NameConfig stores ms).
|
|
255
|
-
let timeoutMs = cfg.timeout;
|
|
256
|
-
if (timeoutMs <= 0) {
|
|
257
|
-
// claude -p cold-start is multi-second; give the CLI path more room.
|
|
258
|
-
timeoutMs = usingCLI ? 15_000 : 3_000;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const controller = new AbortController();
|
|
262
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
263
|
-
|
|
264
|
-
try {
|
|
265
|
-
const raw = await llmFn(cfg.model, resolved, controller.signal);
|
|
266
|
-
const name = sanitizeSlug(raw);
|
|
267
|
-
return name !== undefined ? name : heuristicName(resolved);
|
|
268
|
-
} catch {
|
|
269
|
-
return heuristicName(resolved);
|
|
270
|
-
} finally {
|
|
271
|
-
clearTimeout(timer);
|
|
272
|
-
}
|
|
273
|
-
}
|
package/src/clipboard.ts
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
// Port of src/clipboard.go from the Go reference implementation.
|
|
2
|
-
//
|
|
3
|
-
// pickClipboardTool — detects the platform-appropriate clipboard binary.
|
|
4
|
-
// copyToClipboard — spawns that binary and pipes text into its stdin.
|
|
5
|
-
|
|
6
|
-
// ── types ────────────────────────────────────────────────────────────────────
|
|
7
|
-
|
|
8
|
-
/** Encodes the platform-detected clipboard tool choice. */
|
|
9
|
-
export interface ClipboardTool {
|
|
10
|
-
name: string;
|
|
11
|
-
args: string[];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* SpawnResult is the injectable seam used in tests to avoid exec'ing real
|
|
16
|
-
* clipboard binaries. Production code calls defaultSpawnClipboard.
|
|
17
|
-
*/
|
|
18
|
-
export type ClipboardSpawnFn = (
|
|
19
|
-
name: string,
|
|
20
|
-
args: string[],
|
|
21
|
-
text: string,
|
|
22
|
-
) => Promise<void>;
|
|
23
|
-
|
|
24
|
-
// ── detection ─────────────────────────────────────────────────────────────────
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* pickClipboardTool returns the first tool that matches the current runtime
|
|
28
|
-
* + environment, or null if no supported clipboard integration is available.
|
|
29
|
-
*
|
|
30
|
-
* Detection rules (matches the Go reference):
|
|
31
|
-
*
|
|
32
|
-
* Linux (Wayland — $WAYLAND_DISPLAY set): wl-copy
|
|
33
|
-
* Linux (X11 — $DISPLAY set): xclip -selection clipboard
|
|
34
|
-
* macOS (process.platform === 'darwin'): pbcopy
|
|
35
|
-
* Windows (process.platform === 'win32'): clip
|
|
36
|
-
*
|
|
37
|
-
* Pure function of (platform, env-lookup) — trivially testable without
|
|
38
|
-
* exec'ing anything.
|
|
39
|
-
*/
|
|
40
|
-
export function pickClipboardTool(
|
|
41
|
-
platform: NodeJS.Platform,
|
|
42
|
-
env: (key: string) => string | undefined,
|
|
43
|
-
): ClipboardTool | null {
|
|
44
|
-
switch (platform) {
|
|
45
|
-
case 'linux': {
|
|
46
|
-
if (env('WAYLAND_DISPLAY')) {
|
|
47
|
-
return { name: 'wl-copy', args: [] };
|
|
48
|
-
}
|
|
49
|
-
if (env('DISPLAY')) {
|
|
50
|
-
return { name: 'xclip', args: ['-selection', 'clipboard'] };
|
|
51
|
-
}
|
|
52
|
-
// Headless Linux: no supported tool.
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
case 'darwin':
|
|
56
|
-
return { name: 'pbcopy', args: [] };
|
|
57
|
-
case 'win32':
|
|
58
|
-
return { name: 'clip', args: [] };
|
|
59
|
-
default:
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// ── spawn implementation ──────────────────────────────────────────────────────
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* defaultClipboardSpawn spawns cmd with args and writes text to its stdin
|
|
68
|
-
* using Bun.spawn. Throws on non-zero exit.
|
|
69
|
-
*/
|
|
70
|
-
export const defaultClipboardSpawn: ClipboardSpawnFn = async (name, args, text) => {
|
|
71
|
-
const proc = Bun.spawn([name, ...args], {
|
|
72
|
-
stdin: 'pipe',
|
|
73
|
-
stdout: 'ignore',
|
|
74
|
-
stderr: 'ignore',
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// Write text into stdin then close it.
|
|
78
|
-
const writer = proc.stdin;
|
|
79
|
-
writer.write(text);
|
|
80
|
-
await writer.end();
|
|
81
|
-
|
|
82
|
-
const exitCode = await proc.exited;
|
|
83
|
-
if (exitCode !== 0) {
|
|
84
|
-
throw new Error(`${name} exited with code ${exitCode}`);
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
// ── xsel fallback ────────────────────────────────────────────────────────────
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* xselFallback tries xsel --clipboard --input when xclip is unavailable or
|
|
92
|
-
* fails. Returns true on success, false + error on failure.
|
|
93
|
-
*/
|
|
94
|
-
async function xselFallback(
|
|
95
|
-
text: string,
|
|
96
|
-
spawnFn: ClipboardSpawnFn,
|
|
97
|
-
): Promise<{ ok: boolean; err?: Error }> {
|
|
98
|
-
try {
|
|
99
|
-
await spawnFn('xsel', ['--clipboard', '--input'], text);
|
|
100
|
-
return { ok: true };
|
|
101
|
-
} catch (e) {
|
|
102
|
-
return { ok: false, err: e as Error };
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ── public API ────────────────────────────────────────────────────────────────
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* copyToClipboard writes text to the user's clipboard via the
|
|
110
|
-
* platform-appropriate tool. Returns { ok: true } on success or
|
|
111
|
-
* { ok: false, error } on failure (tool missing, exec error, no detector
|
|
112
|
-
* matched, etc.).
|
|
113
|
-
*
|
|
114
|
-
* Detection precedence and tool choice are documented on pickClipboardTool.
|
|
115
|
-
*/
|
|
116
|
-
export async function copyToClipboard(
|
|
117
|
-
text: string,
|
|
118
|
-
spawnFn: ClipboardSpawnFn = defaultClipboardSpawn,
|
|
119
|
-
platform: NodeJS.Platform = process.platform,
|
|
120
|
-
env: (key: string) => string | undefined = (k) => process.env[k],
|
|
121
|
-
): Promise<{ ok: boolean; error?: Error }> {
|
|
122
|
-
const tool = pickClipboardTool(platform, env);
|
|
123
|
-
if (!tool) {
|
|
124
|
-
return {
|
|
125
|
-
ok: false,
|
|
126
|
-
error: new Error(
|
|
127
|
-
`no clipboard integration available for platform=${platform}`,
|
|
128
|
-
),
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
try {
|
|
133
|
-
await spawnFn(tool.name, tool.args, text);
|
|
134
|
-
return { ok: true };
|
|
135
|
-
} catch (primaryErr) {
|
|
136
|
-
// X11 fallback: try xsel before giving up.
|
|
137
|
-
if (tool.name === 'xclip') {
|
|
138
|
-
const fb = await xselFallback(text, spawnFn);
|
|
139
|
-
if (fb.ok) return { ok: true };
|
|
140
|
-
return {
|
|
141
|
-
ok: false,
|
|
142
|
-
error: new Error(
|
|
143
|
-
`xclip failed (${(primaryErr as Error).message}); xsel fallback failed (${fb.err?.message})`,
|
|
144
|
-
),
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
return { ok: false, error: primaryErr as Error };
|
|
148
|
-
}
|
|
149
|
-
}
|