@fnclaude/cli 0.7.2 → 0.7.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 +215 -27
- package/package.json +1 -1
- package/src/argParser.ts +19 -32
- package/src/args/preserve.ts +2 -2
- package/src/args.ts +200 -23
- package/src/argv.ts +18 -19
- package/src/config.ts +85 -52
- package/src/help.ts +1 -1
- package/src/hostAliases.ts +40 -15
- package/src/index.ts +11 -5
- package/src/main.ts +209 -88
- package/src/mcp/client.ts +12 -9
- package/src/mcp/protocol.ts +66 -26
- package/src/mcp/socketListener.ts +20 -10
- package/src/passthrough.ts +36 -0
- package/src/paths.ts +31 -0
- package/src/prompts.ts +5 -12
- package/src/pty/unix.ts +250 -107
- package/src/pty.ts +20 -13
- package/src/repoSettings.ts +35 -11
- package/src/silentRelaunch.ts +3 -3
- package/src/spawn.ts +2 -28
- package/src/warnings.ts +15 -31
- package/src/worktree.ts +57 -43
package/src/pty.ts
CHANGED
|
@@ -57,18 +57,24 @@ export class RingBuffer {
|
|
|
57
57
|
write(p: Buffer | Uint8Array | string): void {
|
|
58
58
|
const data = typeof p === 'string' ? Buffer.from(p) : Buffer.from(p);
|
|
59
59
|
if (data.length === 0) return;
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
let
|
|
60
|
+
// For oversize writes (> cap) skip ahead — the prefix we'd write would
|
|
61
|
+
// be immediately overwritten by the suffix. Land on a clean state where
|
|
62
|
+
// pos = 0, full = true, and we copy the trailing `cap` bytes in one go.
|
|
63
|
+
let src = 0;
|
|
64
64
|
if (data.length > this.cap) {
|
|
65
|
-
|
|
65
|
+
src = data.length - this.cap;
|
|
66
66
|
this.full = true;
|
|
67
67
|
this.pos = 0;
|
|
68
68
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
// Copy in up to two chunks: from src to end-of-buf, then wrapped around
|
|
70
|
+
// from start-of-buf for the remainder. `Buffer.copy` is a memcpy under
|
|
71
|
+
// the hood — substantially cheaper than the per-byte assignment loop
|
|
72
|
+
// this replaces, for the same final buffer state.
|
|
73
|
+
while (src < data.length) {
|
|
74
|
+
const writable = Math.min(data.length - src, this.cap - this.pos);
|
|
75
|
+
data.copy(this.buf, this.pos, src, src + writable);
|
|
76
|
+
src += writable;
|
|
77
|
+
this.pos = (this.pos + writable) % this.cap;
|
|
72
78
|
if (this.pos === 0) this.full = true;
|
|
73
79
|
}
|
|
74
80
|
}
|
|
@@ -117,16 +123,17 @@ export interface CrossCwdMatch {
|
|
|
117
123
|
* the LAST match wins.
|
|
118
124
|
*/
|
|
119
125
|
export function detectCrossCwd(tail: Buffer): CrossCwdMatch | null {
|
|
120
|
-
// Reset regex internal state — crossCwdRe is `g`-flagged.
|
|
121
|
-
crossCwdRe.lastIndex = 0;
|
|
122
126
|
// Decode as Latin-1 so every byte maps to a code unit; the regex matches
|
|
123
127
|
// ASCII anchors so the multi-byte representation of any non-ASCII bytes
|
|
124
128
|
// never participates in a match. This is the JS equivalent of Go's
|
|
125
129
|
// []byte-scanning behavior.
|
|
126
130
|
const s = tail.toString('latin1');
|
|
127
|
-
|
|
128
|
-
//
|
|
129
|
-
|
|
131
|
+
// matchAll iterates from a fresh internal cursor each call — no
|
|
132
|
+
// module-level `lastIndex` to reset. The exported `crossCwdRe` stays
|
|
133
|
+
// `g`-flagged (matchAll requires it) but is only ever consumed as an
|
|
134
|
+
// anchor for tests / the source-of-truth comparison.
|
|
135
|
+
let last: RegExpMatchArray | null = null;
|
|
136
|
+
for (const m of s.matchAll(crossCwdRe)) {
|
|
130
137
|
last = m;
|
|
131
138
|
}
|
|
132
139
|
if (last === null) return null;
|
package/src/repoSettings.ts
CHANGED
|
@@ -38,6 +38,17 @@ function home(): string {
|
|
|
38
38
|
return process.env.HOME ?? homedir();
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Result of a repo-settings load: the merged settings plus any non-fatal
|
|
43
|
+
* warnings (e.g. malformed JSON files that were skipped). Mirrors
|
|
44
|
+
* `LoadConfigResult` so the caller can thread warnings into the deferred
|
|
45
|
+
* flush.
|
|
46
|
+
*/
|
|
47
|
+
export interface LoadRepoSettingsResult {
|
|
48
|
+
settings: RepoSettings;
|
|
49
|
+
warnings: readonly string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
/**
|
|
42
53
|
* Resolve the four-tier merge for the user's environment.
|
|
43
54
|
* `projectRoot` is the cwd Claude Code anchors project/local tiers
|
|
@@ -46,7 +57,7 @@ function home(): string {
|
|
|
46
57
|
export function loadRepoSettings(
|
|
47
58
|
homeDir: string,
|
|
48
59
|
projectRoot: string,
|
|
49
|
-
):
|
|
60
|
+
): LoadRepoSettingsResult {
|
|
50
61
|
const paths: string[] = [
|
|
51
62
|
join(homeDir, '.claude', 'settings.json'), // user
|
|
52
63
|
join(projectRoot, '.claude', 'settings.json'), // project
|
|
@@ -59,13 +70,17 @@ export function loadRepoSettings(
|
|
|
59
70
|
|
|
60
71
|
/**
|
|
61
72
|
* Read each path (if it exists) and merge per-field with later entries
|
|
62
|
-
* winning over earlier ones. Missing
|
|
63
|
-
*
|
|
73
|
+
* winning over earlier ones. Missing files are silently skipped (the
|
|
74
|
+
* fail-soft posture the plugin matches); malformed files produce a
|
|
75
|
+
* warning so the user can fix them rather than wondering why their
|
|
76
|
+
* settings don't apply.
|
|
64
77
|
*/
|
|
65
|
-
export function mergeRepoSettings(paths: string[]):
|
|
78
|
+
export function mergeRepoSettings(paths: string[]): LoadRepoSettingsResult {
|
|
66
79
|
const merged: RepoSettings = {};
|
|
80
|
+
const warnings: string[] = [];
|
|
67
81
|
for (const p of paths) {
|
|
68
|
-
const f = readRepoSettings(p);
|
|
82
|
+
const { settings: f, warning } = readRepoSettings(p);
|
|
83
|
+
if (warning !== null) warnings.push(warning);
|
|
69
84
|
if (!f) continue;
|
|
70
85
|
// Shallow-merge per field: only overwrite when the higher tier sets
|
|
71
86
|
// a non-empty value.
|
|
@@ -74,23 +89,32 @@ export function mergeRepoSettings(paths: string[]): RepoSettings {
|
|
|
74
89
|
if (f.branchTemplate) merged.branchTemplate = f.branchTemplate;
|
|
75
90
|
if (f.gateEnvVar) merged.gateEnvVar = f.gateEnvVar;
|
|
76
91
|
}
|
|
77
|
-
return merged;
|
|
92
|
+
return { settings: merged, warnings };
|
|
78
93
|
}
|
|
79
94
|
|
|
80
|
-
|
|
95
|
+
interface ReadRepoSettingsResult {
|
|
96
|
+
settings: RepoSettings | null;
|
|
97
|
+
warning: string | null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function readRepoSettings(path: string): ReadRepoSettingsResult {
|
|
81
101
|
let data: string;
|
|
82
102
|
try {
|
|
83
103
|
data = readFileSync(path, 'utf8');
|
|
84
104
|
} catch {
|
|
85
|
-
|
|
105
|
+
// Missing file is the common path — stay silent.
|
|
106
|
+
return { settings: null, warning: null };
|
|
86
107
|
}
|
|
87
108
|
let f: SettingsFile;
|
|
88
109
|
try {
|
|
89
110
|
f = JSON.parse(data) as SettingsFile;
|
|
90
|
-
} catch {
|
|
91
|
-
return
|
|
111
|
+
} catch (err) {
|
|
112
|
+
return {
|
|
113
|
+
settings: null,
|
|
114
|
+
warning: `fnclaude: repo-settings file ${path} is malformed, skipping: ${(err as Error).message}`,
|
|
115
|
+
};
|
|
92
116
|
}
|
|
93
|
-
return f.repoSettings ?? null;
|
|
117
|
+
return { settings: f.repoSettings ?? null, warning: null };
|
|
94
118
|
}
|
|
95
119
|
|
|
96
120
|
/**
|
package/src/silentRelaunch.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { spawn } from 'node:child_process';
|
|
19
19
|
import process from 'node:process';
|
|
20
20
|
import { clearScreen, reconstructArgv } from './pty.js';
|
|
21
|
-
import {
|
|
21
|
+
import { resolveSelfPath } from './paths.js';
|
|
22
22
|
|
|
23
23
|
// `process.execve` is a Bun-native POSIX-only API (1.3.14+). @types/bun
|
|
24
24
|
// hasn't typed it yet — declare the shape inline so TS strict mode is happy.
|
|
@@ -53,7 +53,7 @@ export function silentRelaunch(
|
|
|
53
53
|
): void {
|
|
54
54
|
let self: string;
|
|
55
55
|
try {
|
|
56
|
-
self =
|
|
56
|
+
self = resolveSelfPath();
|
|
57
57
|
} catch (err) {
|
|
58
58
|
process.stderr.write(
|
|
59
59
|
`fnclaude: cannot determine executable, cannot relaunch: ${(err as Error).message}\n`,
|
|
@@ -84,7 +84,7 @@ export function silentRelaunchHandoff(
|
|
|
84
84
|
): void {
|
|
85
85
|
let self: string;
|
|
86
86
|
try {
|
|
87
|
-
self =
|
|
87
|
+
self = resolveSelfPath();
|
|
88
88
|
} catch (err) {
|
|
89
89
|
process.stderr.write(
|
|
90
90
|
`fnclaude: cannot determine executable, cannot relaunch: ${(err as Error).message}\n`,
|
package/src/spawn.ts
CHANGED
|
@@ -9,10 +9,9 @@
|
|
|
9
9
|
// The indirection via spawnFn lets tests inject a mock without launching
|
|
10
10
|
// real processes.
|
|
11
11
|
|
|
12
|
-
import { realpathSync } from 'node:fs';
|
|
13
|
-
import { dirname } from 'node:path';
|
|
14
12
|
import process from 'node:process';
|
|
15
13
|
import type { Config } from './config.js';
|
|
14
|
+
import { resolveSelfPath } from './paths.js';
|
|
16
15
|
import { substitute } from './template.js';
|
|
17
16
|
|
|
18
17
|
// ── env cleaning ───────────────────────────────────────────────────────────
|
|
@@ -39,31 +38,6 @@ export function cleanEnvForSpawn(env: string[]): string[] {
|
|
|
39
38
|
return out;
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
// ── selfPath ───────────────────────────────────────────────────────────────
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Return the absolute, symlink-resolved path to this fnclaude script,
|
|
46
|
-
* suitable for `{bin}` substitution in a spawn-launcher template.
|
|
47
|
-
*
|
|
48
|
-
* Preference order mirrors prompts.ts (Unit 6):
|
|
49
|
-
* 1. process.argv[1] — the CLI script path (anchors to the script, not the
|
|
50
|
-
* Bun interpreter).
|
|
51
|
-
* 2. process.execPath — the Bun binary; fallback when argv[1] is absent.
|
|
52
|
-
*
|
|
53
|
-
* Symlinks are resolved so the spawned launcher gets the real path, not a
|
|
54
|
-
* shim that might not be on the PATH inside the new window.
|
|
55
|
-
*/
|
|
56
|
-
export function selfPath(): string {
|
|
57
|
-
const argv1 = process.argv.length > 1 ? process.argv[1] : undefined;
|
|
58
|
-
let exe = argv1 !== undefined && argv1 !== '' ? argv1 : process.execPath;
|
|
59
|
-
try {
|
|
60
|
-
exe = realpathSync(exe);
|
|
61
|
-
} catch {
|
|
62
|
-
// symlink resolution failure is not fatal — use the unresolved path
|
|
63
|
-
}
|
|
64
|
-
return exe;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
41
|
// ── autoDetectSpawnCommand ─────────────────────────────────────────────────
|
|
68
42
|
|
|
69
43
|
/**
|
|
@@ -162,7 +136,7 @@ export async function spawnSibling(
|
|
|
162
136
|
extraArgs: string[],
|
|
163
137
|
spawnFn: SpawnFn = defaultSpawnFn,
|
|
164
138
|
): Promise<boolean> {
|
|
165
|
-
const bin =
|
|
139
|
+
const bin = resolveSelfPath();
|
|
166
140
|
|
|
167
141
|
let tmpl = cfg.auto.spawnCommand;
|
|
168
142
|
if (!tmpl) {
|
package/src/warnings.ts
CHANGED
|
@@ -5,46 +5,30 @@
|
|
|
5
5
|
// scroll off-screen too fast to read; flushing on exit shows them in the
|
|
6
6
|
// user's shell where they have time to actually be seen.
|
|
7
7
|
//
|
|
8
|
+
// There is no module-global queue here — every loader (loadConfig,
|
|
9
|
+
// loadRepoSettings, loadHostAliases, loadPrompts) returns its warnings
|
|
10
|
+
// alongside its result, and `main.ts` threads them into a single local
|
|
11
|
+
// list that `flushWarnings` drains at the deferred-flush point. The old
|
|
12
|
+
// global queue made test fixtures share state across files and forced
|
|
13
|
+
// callers to know about a sink module they otherwise didn't depend on;
|
|
14
|
+
// the explicit-thread shape is the fix.
|
|
15
|
+
//
|
|
8
16
|
// Fatal errors that prevent launch entirely (e.g. claude binary not on PATH)
|
|
9
17
|
// should still print directly to stderr and exit non-zero — those don't
|
|
10
18
|
// need deferring because there's no claude session about to drown them out.
|
|
11
19
|
|
|
12
20
|
import process from 'node:process';
|
|
13
21
|
|
|
14
|
-
const warnings: string[] = [];
|
|
15
|
-
|
|
16
22
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
23
|
+
* Print each warning to `stream` on its own line. Returns the number of
|
|
24
|
+
* warnings written (useful for tests). Empty input is a no-op.
|
|
19
25
|
*/
|
|
20
|
-
export function
|
|
21
|
-
warnings
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Print all queued warnings to stderr in order, then clear the queue.
|
|
26
|
-
* Called from `run()` after claude exits. Returns the number of warnings
|
|
27
|
-
* that were flushed (useful for tests).
|
|
28
|
-
*/
|
|
29
|
-
export function flushWarnings(stream: NodeJS.WriteStream = process.stderr): number {
|
|
30
|
-
const n = warnings.length;
|
|
26
|
+
export function flushWarnings(
|
|
27
|
+
warnings: readonly string[],
|
|
28
|
+
stream: NodeJS.WriteStream = process.stderr,
|
|
29
|
+
): number {
|
|
31
30
|
for (const w of warnings) {
|
|
32
31
|
stream.write(`${w}\n`);
|
|
33
32
|
}
|
|
34
|
-
warnings.length
|
|
35
|
-
return n;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Test helper: return a snapshot of the current queue without flushing.
|
|
40
|
-
*/
|
|
41
|
-
export function pendingWarnings(): readonly string[] {
|
|
42
|
-
return [...warnings];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Test helper: clear the queue without emitting anything.
|
|
47
|
-
*/
|
|
48
|
-
export function clearWarnings(): void {
|
|
49
|
-
warnings.length = 0;
|
|
33
|
+
return warnings.length;
|
|
50
34
|
}
|
package/src/worktree.ts
CHANGED
|
@@ -8,14 +8,18 @@
|
|
|
8
8
|
// pass a GitRunner in, with `defaultGitRunner` exported for production
|
|
9
9
|
// use. Both shapes give tests deterministic control without an env or
|
|
10
10
|
// module-state assumption.
|
|
11
|
-
// - applyWorktreeIntercept
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
11
|
+
// - applyWorktreeIntercept is a pure stage transition: takes a
|
|
12
|
+
// `ResolvedArgs`, returns an `InterceptedArgs` that carries the new
|
|
13
|
+
// `worktreeMatched` invariant. The cwd / passthrough overrides flow
|
|
14
|
+
// through `withIntercepted`; nothing is mutated in place.
|
|
15
15
|
|
|
16
|
-
import { execFileSync } from 'node:child_process';
|
|
17
16
|
import { isAbsolute, join } from 'node:path';
|
|
18
|
-
import
|
|
17
|
+
import {
|
|
18
|
+
withIntercepted,
|
|
19
|
+
type InterceptedArgs,
|
|
20
|
+
type ResolvedArgs,
|
|
21
|
+
} from './args.js';
|
|
22
|
+
import { nameInPassthrough } from './passthrough.js';
|
|
19
23
|
|
|
20
24
|
/**
|
|
21
25
|
* GitRunner is a thin wrapper around `git -C <dir> <args...>`. Returns the
|
|
@@ -26,14 +30,30 @@ import type { Args } from './args.js';
|
|
|
26
30
|
export type GitRunner = (dir: string, ...args: string[]) => string;
|
|
27
31
|
|
|
28
32
|
/**
|
|
29
|
-
* Production GitRunner. Spawns git synchronously
|
|
30
|
-
*
|
|
33
|
+
* Production GitRunner. Spawns git synchronously via Bun.spawnSync — same
|
|
34
|
+
* mechanism the rest of the codebase uses for its child-process work
|
|
35
|
+
* (autoname, resolver, clipboard, spawn). Node's `execFileSync` here was
|
|
36
|
+
* the lone holdout; switching unifies the spawn layer.
|
|
37
|
+
*
|
|
38
|
+
* Behaviour preserved from the prior `execFileSync` version:
|
|
39
|
+
* - On a successful run (exit 0), returns the UTF-8-decoded stdout.
|
|
40
|
+
* - On non-zero exit, throws an Error with the git stderr verbatim —
|
|
41
|
+
* callers (listWorktrees) catch any thrown value and treat it as
|
|
42
|
+
* "no match possible", so the exact shape of the error doesn't matter
|
|
43
|
+
* beyond being throwable.
|
|
44
|
+
* - If `git` isn't on PATH, Bun.spawnSync throws ENOENT itself (same
|
|
45
|
+
* posture as execFileSync did).
|
|
31
46
|
*/
|
|
32
47
|
export const defaultGitRunner: GitRunner = (dir, ...args) => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
48
|
+
const proc = Bun.spawnSync(['git', '-C', dir, ...args], {
|
|
49
|
+
stdout: 'pipe',
|
|
50
|
+
stderr: 'pipe',
|
|
36
51
|
});
|
|
52
|
+
if (proc.exitCode !== 0) {
|
|
53
|
+
const stderr = proc.stderr?.toString('utf8') ?? '';
|
|
54
|
+
throw new Error(`git -C ${dir} ${args.join(' ')} exited ${proc.exitCode}: ${stderr.trim()}`);
|
|
55
|
+
}
|
|
56
|
+
return proc.stdout?.toString('utf8') ?? '';
|
|
37
57
|
};
|
|
38
58
|
|
|
39
59
|
/**
|
|
@@ -127,30 +147,37 @@ function basename(p: string): string {
|
|
|
127
147
|
}
|
|
128
148
|
|
|
129
149
|
/**
|
|
130
|
-
* applyWorktreeIntercept applies the -w / --worktree intercept logic
|
|
131
|
-
*
|
|
150
|
+
* applyWorktreeIntercept applies the -w / --worktree intercept logic.
|
|
151
|
+
*
|
|
152
|
+
* Pure function: takes a `ResolvedArgs`, returns a new `InterceptedArgs`.
|
|
153
|
+
* No input is mutated. The four cases:
|
|
132
154
|
*
|
|
133
|
-
* 1. worktreeSet=false →
|
|
134
|
-
* 2. Bare -w (worktreeArg="") →
|
|
135
|
-
*
|
|
155
|
+
* 1. worktreeSet=false → carry through with worktreeMatched=false.
|
|
156
|
+
* 2. Bare -w (worktreeArg="") → append --worktree to passthrough,
|
|
157
|
+
* worktreeMatched=false.
|
|
158
|
+
* 3. Existing worktree matched → swap cwd to the worktree path, set
|
|
136
159
|
* worktreeMatched=true, suppress --worktree.
|
|
137
|
-
* 4. Otherwise →
|
|
138
|
-
*
|
|
160
|
+
* 4. Otherwise → append --worktree <name>, plus --name <name> when
|
|
161
|
+
* --name isn't already set; worktreeMatched=false.
|
|
139
162
|
*
|
|
140
163
|
* `shellCWD` is the process working directory at fnclaude startup, used
|
|
141
|
-
* to resolve a relative
|
|
164
|
+
* to resolve a relative `cwd` to an absolute path before querying git.
|
|
142
165
|
*/
|
|
143
166
|
export function applyWorktreeIntercept(
|
|
144
|
-
a:
|
|
167
|
+
a: ResolvedArgs,
|
|
145
168
|
shellCWD: string,
|
|
146
169
|
runner: GitRunner = defaultGitRunner,
|
|
147
|
-
):
|
|
148
|
-
if (!a.worktreeSet)
|
|
170
|
+
): InterceptedArgs {
|
|
171
|
+
if (!a.worktreeSet) {
|
|
172
|
+
return withIntercepted(a, { worktreeMatched: false });
|
|
173
|
+
}
|
|
149
174
|
|
|
150
175
|
// Bare -w with no name: push --worktree back through unchanged.
|
|
151
176
|
if (a.worktreeArg === '') {
|
|
152
|
-
a
|
|
153
|
-
|
|
177
|
+
return withIntercepted(a, {
|
|
178
|
+
passthrough: [...a.passthrough, '--worktree'],
|
|
179
|
+
worktreeMatched: false,
|
|
180
|
+
});
|
|
154
181
|
}
|
|
155
182
|
|
|
156
183
|
// Resolve absolute cwd for git queries.
|
|
@@ -161,26 +188,13 @@ export function applyWorktreeIntercept(
|
|
|
161
188
|
const hit = findWorktree(listWorktrees(dir, runner), a.worktreeArg);
|
|
162
189
|
if (hit) {
|
|
163
190
|
// Existing worktree matched: swap cwd, suppress -w.
|
|
164
|
-
a
|
|
165
|
-
a.worktreeMatched = true;
|
|
166
|
-
return;
|
|
191
|
+
return withIntercepted(a, { cwd: hit.path, worktreeMatched: true });
|
|
167
192
|
}
|
|
168
193
|
|
|
169
194
|
// No match (or not a repo): pass --worktree through and attach --name.
|
|
170
|
-
a.passthrough
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* nameInPassthrough — local copy of the helper in argParser.ts. Replicated
|
|
178
|
-
* to avoid an import cycle (argParser → buildArgv → worktree → argParser).
|
|
179
|
-
* Both copies must agree; the shared contract is "--name or -n, bare or
|
|
180
|
-
* =value, anywhere in the slice."
|
|
181
|
-
*/
|
|
182
|
-
function nameInPassthrough(passthrough: readonly string[]): boolean {
|
|
183
|
-
return passthrough.some(
|
|
184
|
-
(t) => t === '--name' || t === '-n' || t.startsWith('--name=') || t.startsWith('-n='),
|
|
185
|
-
);
|
|
195
|
+
const withWt = [...a.passthrough, '--worktree', a.worktreeArg];
|
|
196
|
+
const passthrough = nameInPassthrough(withWt)
|
|
197
|
+
? withWt
|
|
198
|
+
: [...withWt, '--name', a.worktreeArg];
|
|
199
|
+
return withIntercepted(a, { passthrough, worktreeMatched: false });
|
|
186
200
|
}
|