@fnclaude/cli 1.1.1 → 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.
Files changed (100) hide show
  1. package/bin/fnc.js +34 -79
  2. package/package.json +6 -9
  3. package/share/fnclaude/templates/handoff.template.md +11 -0
  4. package/src/argv/classify.ts +48 -0
  5. package/src/argv/expand.ts +51 -0
  6. package/src/argv/intake.ts +52 -0
  7. package/src/argv/magic.ts +103 -0
  8. package/src/argv/parse.ts +213 -0
  9. package/src/argv/preserve-args.ts +333 -0
  10. package/src/argv/sentinel.ts +41 -0
  11. package/src/argv/short-flags.ts +152 -0
  12. package/src/config/load.ts +116 -0
  13. package/src/handoff/awaiter.ts +140 -0
  14. package/src/handoff/clean-env.ts +45 -0
  15. package/src/handoff/kill-and-exec.ts +110 -0
  16. package/src/handoff/spawn-launcher.ts +185 -0
  17. package/src/handoff/summary-file.ts +86 -0
  18. package/src/handoff/trigger.ts +90 -0
  19. package/src/help-version.ts +151 -0
  20. package/src/launch/compose-env.ts +34 -0
  21. package/src/launch/cross-cwd-parse.ts +69 -0
  22. package/src/launch/cross-cwd-relaunch.ts +95 -0
  23. package/src/launch/find-claude.ts +52 -0
  24. package/src/launch/live-permission-reader.ts +133 -0
  25. package/src/launch/ring-buffer.ts +92 -0
  26. package/src/main.ts +580 -437
  27. package/src/mcp/dispatch.ts +240 -0
  28. package/src/mcp/handlers/clipboard-backends.ts +176 -0
  29. package/src/mcp/handlers/clipboard.ts +62 -0
  30. package/src/mcp/handlers/restart.ts +156 -0
  31. package/src/mcp/handlers/spawn.ts +219 -0
  32. package/src/mcp/handlers/switch.ts +272 -0
  33. package/src/mcp/inject-config.ts +59 -0
  34. package/src/mcp/jsonrpc-server.ts +154 -0
  35. package/src/mcp/listener.ts +141 -0
  36. package/src/mcp/parent-dispatch.ts +154 -0
  37. package/src/mcp/socket-path.ts +48 -0
  38. package/src/mcp/wire.ts +181 -0
  39. package/src/name/auto-name.ts +162 -0
  40. package/src/name/llm-prompt.ts +14 -0
  41. package/src/name/sanitize.ts +57 -0
  42. package/src/name/sdk-llm.ts +42 -0
  43. package/src/noop/seed.ts +63 -0
  44. package/src/noop/template-source.ts +62 -0
  45. package/src/path/ensure-cwd.ts +95 -0
  46. package/src/path/resolve.ts +58 -0
  47. package/src/prompts/dir.ts +61 -0
  48. package/src/prompts/load.ts +100 -0
  49. package/src/prompts/select.ts +43 -0
  50. package/src/repo/clone-exec.ts +37 -0
  51. package/src/repo/clone.ts +45 -0
  52. package/src/repo/gh-runner.ts +68 -0
  53. package/src/repo/host-aliases.ts +58 -0
  54. package/src/repo/owner-lookup.ts +71 -0
  55. package/src/repo/ref.ts +146 -0
  56. package/src/repo/repo-settings.ts +99 -0
  57. package/src/repo/resolve-input.ts +179 -0
  58. package/src/repo/template.ts +92 -0
  59. package/src/warnings/buffer.ts +39 -0
  60. package/src/worktree/auto-tmux.ts +45 -0
  61. package/src/worktree/git-list.ts +73 -0
  62. package/src/worktree/intercept.ts +150 -0
  63. package/bin/preflight.js +0 -66
  64. package/prompts/agent-pitfall.md +0 -1
  65. package/prompts/noop-router.md +0 -186
  66. package/prompts/project-switch.md +0 -64
  67. package/prompts/restart.md +0 -50
  68. package/prompts/spawn.md +0 -62
  69. package/src/argParser.ts +0 -367
  70. package/src/args/preserve.ts +0 -338
  71. package/src/args.ts +0 -239
  72. package/src/argv.ts +0 -219
  73. package/src/autoname.ts +0 -273
  74. package/src/clipboard.ts +0 -149
  75. package/src/config.ts +0 -369
  76. package/src/errors.ts +0 -13
  77. package/src/handoff.ts +0 -108
  78. package/src/help.ts +0 -139
  79. package/src/hostAliases.ts +0 -139
  80. package/src/index.ts +0 -120
  81. package/src/mcp/client.ts +0 -645
  82. package/src/mcp/protocol.ts +0 -445
  83. package/src/mcp/socketListener.ts +0 -540
  84. package/src/noop.ts +0 -106
  85. package/src/passthrough.ts +0 -36
  86. package/src/paths.ts +0 -55
  87. package/src/prompts.ts +0 -279
  88. package/src/pty/unix.ts +0 -429
  89. package/src/pty/windows.ts +0 -125
  90. package/src/pty.ts +0 -380
  91. package/src/repoRef.ts +0 -158
  92. package/src/repoSettings.ts +0 -144
  93. package/src/resolver.ts +0 -519
  94. package/src/sanitize.ts +0 -120
  95. package/src/sessionState.ts +0 -220
  96. package/src/silentRelaunch.ts +0 -178
  97. package/src/spawn.ts +0 -163
  98. package/src/template.ts +0 -44
  99. package/src/warnings.ts +0 -34
  100. package/src/worktree.ts +0 -201
package/src/worktree.ts DELETED
@@ -1,201 +0,0 @@
1
- // Port of the worktree-intercept block of src/main.go (Go reference, lines
2
- // 623–743). Mirrors gitRunner / worktreeInfo / listWorktrees / findWorktree /
3
- // applyWorktreeIntercept.
4
- //
5
- // Design notes:
6
- // - The Go reference uses a package-level mutable `gitRunner` var that
7
- // tests swap out. TS port uses explicit dependency injection — callers
8
- // pass a GitRunner in, with `defaultGitRunner` exported for production
9
- // use. Both shapes give tests deterministic control without an env or
10
- // module-state assumption.
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
-
16
- import { isAbsolute, join } from 'node:path';
17
- import {
18
- withIntercepted,
19
- type InterceptedArgs,
20
- type ResolvedArgs,
21
- } from './args.js';
22
- import { nameInPassthrough } from './passthrough.js';
23
-
24
- /**
25
- * GitRunner is a thin wrapper around `git -C <dir> <args...>`. Returns the
26
- * raw stdout string on success; throws on any git error. The thrown-error
27
- * path is treated identically to "no match possible" by callers — they
28
- * never inspect the error itself, so any thrown value is fine.
29
- */
30
- export type GitRunner = (dir: string, ...args: string[]) => string;
31
-
32
- /**
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).
46
- */
47
- export const defaultGitRunner: GitRunner = (dir, ...args) => {
48
- const proc = Bun.spawnSync(['git', '-C', dir, ...args], {
49
- stdout: 'pipe',
50
- stderr: 'pipe',
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') ?? '';
57
- };
58
-
59
- /**
60
- * WorktreeInfo is one entry from `git worktree list --porcelain`.
61
- */
62
- export interface WorktreeInfo {
63
- /** Absolute filesystem path of the worktree. */
64
- path: string;
65
- /**
66
- * Bare branch name (e.g. "feat-x" or "worktree-feat-x"); undefined if the
67
- * worktree is detached.
68
- */
69
- branch: string | undefined;
70
- }
71
-
72
- /**
73
- * listWorktrees runs `git worktree list --porcelain` in dir and parses each
74
- * blank-line-separated block into a WorktreeInfo. Returns [] on any git
75
- * error (not-a-repo, git unavailable, etc.) — callers treat [] as "no
76
- * match possible".
77
- */
78
- export function listWorktrees(dir: string, runner: GitRunner = defaultGitRunner): WorktreeInfo[] {
79
- let out: string;
80
- try {
81
- out = runner(dir, 'worktree', 'list', '--porcelain');
82
- } catch {
83
- return [];
84
- }
85
-
86
- const result: WorktreeInfo[] = [];
87
- for (const block of out.split('\n\n')) {
88
- let path = '';
89
- let branch: string | undefined;
90
- for (const line of block.split('\n')) {
91
- if (line.startsWith('worktree ')) {
92
- path = line.slice('worktree '.length).trim();
93
- } else if (line.startsWith('branch refs/heads/')) {
94
- branch = line.slice('branch refs/heads/'.length).trim();
95
- }
96
- }
97
- if (path !== '') {
98
- result.push({ path, branch });
99
- }
100
- }
101
- return result;
102
- }
103
-
104
- /**
105
- * findWorktree picks the WorktreeInfo matching `query`, trying three
106
- * strategies in order. Branch name is checked first since the branch is
107
- * the semantically stable identifier — its path can be anywhere the
108
- * creator chose, but its branch is the same string the user typed at
109
- * creation time.
110
- *
111
- * 1. Branch name == query (any worktree, any convention)
112
- * 2. Branch with `worktree-` prefix stripped == query (matches Claude's
113
- * default `worktree-<name>` branches)
114
- * 3. Basename of the path == query (last-resort fallback for worktrees
115
- * whose branch was renamed or whose creator skipped the convention)
116
- *
117
- * Returns undefined when no entry matches. Undefined `query` short-circuits
118
- * to undefined so that detached worktrees (branch=undefined) can't be
119
- * matched by accident.
120
- */
121
- export function findWorktree(
122
- worktrees: readonly WorktreeInfo[],
123
- query: string | undefined,
124
- ): WorktreeInfo | undefined {
125
- if (query === undefined) return undefined;
126
-
127
- for (const wt of worktrees) {
128
- if (wt.branch === query) return wt;
129
- }
130
- for (const wt of worktrees) {
131
- if (wt.branch !== undefined && stripWorktreePrefix(wt.branch) === query) return wt;
132
- }
133
- for (const wt of worktrees) {
134
- if (basename(wt.path) === query) return wt;
135
- }
136
- return undefined;
137
- }
138
-
139
- function stripWorktreePrefix(branch: string): string {
140
- return branch.startsWith('worktree-') ? branch.slice('worktree-'.length) : branch;
141
- }
142
-
143
- function basename(p: string): string {
144
- // `path.basename` collapses trailing slashes the way we want here; the
145
- // Go reference uses filepath.Base which has the same shape.
146
- const idx = p.lastIndexOf('/');
147
- return idx === -1 ? p : p.slice(idx + 1);
148
- }
149
-
150
- /**
151
- * applyWorktreeIntercept applies the -w / --worktree intercept logic.
152
- *
153
- * Pure function: takes a `ResolvedArgs`, returns a new `InterceptedArgs`.
154
- * No input is mutated. The four cases:
155
- *
156
- * 1. worktreeSet=false → carry through with worktreeMatched=false.
157
- * 2. Bare -w (worktreeArg=undefined) → append --worktree to passthrough,
158
- * worktreeMatched=false.
159
- * 3. Existing worktree matched → swap cwd to the worktree path, set
160
- * worktreeMatched=true, suppress --worktree.
161
- * 4. Otherwise → append --worktree <name>, plus --name <name> when
162
- * --name isn't already set; worktreeMatched=false.
163
- *
164
- * `shellCWD` is the process working directory at fnclaude startup, used
165
- * to resolve a relative `cwd` to an absolute path before querying git.
166
- */
167
- export function applyWorktreeIntercept(
168
- a: ResolvedArgs,
169
- shellCWD: string,
170
- runner: GitRunner = defaultGitRunner,
171
- ): InterceptedArgs {
172
- if (!a.worktreeSet) {
173
- return withIntercepted(a, { worktreeMatched: false });
174
- }
175
-
176
- // Bare -w with no name: push --worktree back through unchanged.
177
- if (a.worktreeArg === undefined) {
178
- return withIntercepted(a, {
179
- passthrough: [...a.passthrough, '--worktree'],
180
- worktreeMatched: false,
181
- });
182
- }
183
-
184
- // Resolve absolute cwd for git queries.
185
- const dir = isAbsolute(a.cwd) ? a.cwd : join(shellCWD, a.cwd);
186
-
187
- // List worktrees in the project repo, then match the user's query against
188
- // branch / stripped-branch / basename.
189
- const hit = findWorktree(listWorktrees(dir, runner), a.worktreeArg);
190
- if (hit) {
191
- // Existing worktree matched: swap cwd, suppress -w.
192
- return withIntercepted(a, { cwd: hit.path, worktreeMatched: true });
193
- }
194
-
195
- // No match (or not a repo): pass --worktree through and attach --name.
196
- const withWt = [...a.passthrough, '--worktree', a.worktreeArg];
197
- const passthrough = nameInPassthrough(withWt)
198
- ? withWt
199
- : [...withWt, '--name', a.worktreeArg];
200
- return withIntercepted(a, { passthrough, worktreeMatched: false });
201
- }