@fnclaude/cli 1.0.1 → 1.1.1
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 +79 -14
- package/bin/preflight.js +66 -0
- package/package.json +1 -1
- package/src/argv.ts +16 -0
package/bin/fnc.js
CHANGED
|
@@ -1,24 +1,89 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
2
|
-
// fnclaude entry point —
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @fnclaude/cli entry point — owns the Node→Bun preflight and re-execs
|
|
3
|
+
// itself under Bun when needed.
|
|
3
4
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
5
|
+
// Why Node-shebang for a Bun cli: when `npm i -g @fnclaude/cli` puts
|
|
6
|
+
// `fnc` on PATH, the user's shell invokes it under whichever runtime npm
|
|
7
|
+
// happened to link against — typically Node, since npm itself runs under
|
|
8
|
+
// Node. We must therefore be loadable under Node, do our own runtime
|
|
9
|
+
// check, and re-exec under Bun ourselves. This shim used to live in the
|
|
10
|
+
// umbrella package (`fnclaude`), which meant standalone installs of
|
|
11
|
+
// `@fnclaude/cli` skipped it and silently degraded (and, on the Bun
|
|
12
|
+
// side, hit the `--`-stripping bug). Owning the preflight here is the
|
|
13
|
+
// single source of truth.
|
|
9
14
|
//
|
|
10
15
|
// Module resolution: dist/main.js is what npm-installed users execute;
|
|
11
|
-
// local devs running from source use Bun's TS support to import
|
|
12
|
-
// directly. We attempt dist first and fall back to src so
|
|
13
|
-
// are first-class without a separate dev shim.
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
+
// local devs running from source use Bun's TS support to import
|
|
17
|
+
// src/main.ts directly. We attempt dist first and fall back to src so
|
|
18
|
+
// both workflows are first-class without a separate dev shim.
|
|
19
|
+
import { spawnSync } from 'node:child_process';
|
|
20
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
21
|
+
import { dirname, join, resolve } from 'node:path';
|
|
16
22
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
23
|
+
import { decide, defaultLookupBun } from './preflight.js';
|
|
17
24
|
|
|
18
|
-
const
|
|
25
|
+
const selfPath = fileURLToPath(import.meta.url);
|
|
26
|
+
const here = dirname(selfPath);
|
|
27
|
+
|
|
28
|
+
// Short-circuit --version BEFORE the Bun preflight. Reading package.json
|
|
29
|
+
// is pure Node-stdlib work and doesn't need Bun — friendlier when
|
|
30
|
+
// someone is diagnosing a broken install without Bun on PATH.
|
|
31
|
+
const argv = process.argv.slice(2);
|
|
32
|
+
if (argv.includes('--version') || argv.includes('-v')) {
|
|
33
|
+
const pkg = JSON.parse(
|
|
34
|
+
readFileSync(join(here, '..', 'package.json'), 'utf8'),
|
|
35
|
+
);
|
|
36
|
+
process.stdout.write(`fnclaude ${pkg.version}\n`);
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const decision = decide({
|
|
41
|
+
hasBun: typeof globalThis.Bun !== 'undefined',
|
|
42
|
+
lookupBun: defaultLookupBun,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (decision.kind === 'error') {
|
|
46
|
+
process.stderr.write(`${decision.message}\n`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (decision.kind === 'reexec') {
|
|
51
|
+
// Re-launch ourselves under Bun. `spawnSync` with stdio:'inherit'
|
|
52
|
+
// forwards streams transparently; we propagate the child's exit code
|
|
53
|
+
// so the OS / parent process sees the same status it would have seen
|
|
54
|
+
// had Bun been the launcher all along.
|
|
55
|
+
//
|
|
56
|
+
// Argv-via-env: Bun strips the first `--` from a script's argv,
|
|
57
|
+
// regardless of where it appears (script invocation, `bun --`, `bun
|
|
58
|
+
// run`, shebang). Confirmed empirically. So passing the user's args
|
|
59
|
+
// as Bun-script argv would silently mangle `fnc -- "prompt"` into
|
|
60
|
+
// `fnc "prompt"` — the cli then treats the prompt as a cwd
|
|
61
|
+
// positional, the resolver fires, and 8 GitHub orgs 404 in series
|
|
62
|
+
// before the user gets a misleading "could not resolve" error. We
|
|
63
|
+
// sidestep by serialising the user's args into FNC_ARGS_JSON; main()
|
|
64
|
+
// reads from there when present (and deletes the env var to avoid
|
|
65
|
+
// leaking to its own children).
|
|
66
|
+
const r = spawnSync(decision.bun, [selfPath], {
|
|
67
|
+
stdio: 'inherit',
|
|
68
|
+
env: {
|
|
69
|
+
...process.env,
|
|
70
|
+
FNC_ARGS_JSON: JSON.stringify(argv),
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
if (r.error) {
|
|
74
|
+
// ENOENT shouldn't reach here — lookupBun confirmed bun is reachable
|
|
75
|
+
// — but if something else broke (EACCES, ETXTBSY), surface it
|
|
76
|
+
// instead of swallowing.
|
|
77
|
+
process.stderr.write(`fnclaude: failed to re-exec under bun: ${r.error.message}\n`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
process.exit(r.status ?? 0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// decision.kind === 'run' — we're already under Bun. Import main() and
|
|
84
|
+
// dispatch.
|
|
19
85
|
const distMain = resolve(here, '..', 'dist', 'main.js');
|
|
20
86
|
const srcMain = resolve(here, '..', 'src', 'main.ts');
|
|
21
|
-
|
|
22
87
|
const target = existsSync(distMain) ? distMain : srcMain;
|
|
23
88
|
const { main } = await import(pathToFileURL(target).href);
|
|
24
89
|
|
package/bin/preflight.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Preflight for the cli `fnc` bin shim — decide whether to proceed,
|
|
2
|
+
// re-exec under Bun, or hard-error out with a directive message.
|
|
3
|
+
//
|
|
4
|
+
// The cli relies on Bun-only globals (`Bun.spawn`, `Bun.TOML.parse`,
|
|
5
|
+
// `Bun.which`, `process.execve`). When the bin is invoked under Node
|
|
6
|
+
// (typically because `npm i -g @fnclaude/cli` puts it on PATH and the
|
|
7
|
+
// user runs `fnc`), those Bun calls fail silently — `Bun.which` returns
|
|
8
|
+
// `undefined`, `Bun.TOML.parse` throws "Bun is not defined", etc. The
|
|
9
|
+
// cli then degrades into a "claude not found" / config-broken state
|
|
10
|
+
// that LOOKS like a normal failure but is actually a runtime mismatch.
|
|
11
|
+
//
|
|
12
|
+
// This preflight runs first and either re-execs under Bun (if `bun` is
|
|
13
|
+
// on PATH) or prints a directive error and exits non-zero — never the
|
|
14
|
+
// silent-degradation path.
|
|
15
|
+
//
|
|
16
|
+
// Extracted from the shim itself to keep it unit-testable: the decision
|
|
17
|
+
// function takes injected seams (typeof-Bun probe + PATH lookup) and
|
|
18
|
+
// returns a discriminated union; the shim handles the dispatch.
|
|
19
|
+
|
|
20
|
+
import { spawnSync } from 'node:child_process';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {{ kind: 'run' }
|
|
24
|
+
* | { kind: 'reexec', bun: string }
|
|
25
|
+
* | { kind: 'error', message: string }
|
|
26
|
+
* } PreflightDecision
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Decide what the shim should do.
|
|
31
|
+
*
|
|
32
|
+
* @param {{ hasBun: boolean, lookupBun: () => string | null }} seams
|
|
33
|
+
* @returns {PreflightDecision}
|
|
34
|
+
*/
|
|
35
|
+
export function decide({ hasBun, lookupBun }) {
|
|
36
|
+
if (hasBun) return { kind: 'run' };
|
|
37
|
+
const bun = lookupBun();
|
|
38
|
+
if (bun !== null) return { kind: 'reexec', bun };
|
|
39
|
+
return {
|
|
40
|
+
kind: 'error',
|
|
41
|
+
message:
|
|
42
|
+
'fnclaude: this CLI requires Bun (https://bun.sh) to run.\n' +
|
|
43
|
+
' The shim was invoked under Node, but the CLI depends on Bun-only\n' +
|
|
44
|
+
' globals (Bun.spawn, Bun.TOML.parse, process.execve). Install Bun\n' +
|
|
45
|
+
' and re-run `fnc`, or invoke the script via `bun ...` directly.',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Default PATH lookup for `bun`. Returns the literal string `'bun'` on
|
|
51
|
+
* success (the OS will resolve it via PATH at spawn time), or null when
|
|
52
|
+
* `bun` is not reachable.
|
|
53
|
+
*
|
|
54
|
+
* Implementation note: `spawnSync('bun', ['--version'])` is the simplest
|
|
55
|
+
* cross-platform probe. ENOENT comes back as `result.error.code`, and a
|
|
56
|
+
* present-but-broken bun returns non-zero status. Both are treated as
|
|
57
|
+
* "not usable."
|
|
58
|
+
*
|
|
59
|
+
* @returns {string | null}
|
|
60
|
+
*/
|
|
61
|
+
export function defaultLookupBun() {
|
|
62
|
+
const r = spawnSync('bun', ['--version'], { stdio: 'ignore' });
|
|
63
|
+
if (r.error) return null;
|
|
64
|
+
if (r.status !== 0) return null;
|
|
65
|
+
return 'bun';
|
|
66
|
+
}
|
package/package.json
CHANGED
package/src/argv.ts
CHANGED
|
@@ -86,6 +86,13 @@ export function buildFnclaudeMCPConfigJSON(noop: boolean): string | null {
|
|
|
86
86
|
* appended to that existing value. Empty fragments are dropped. Returns
|
|
87
87
|
* `passthrough` unchanged when no non-empty fragments remain.
|
|
88
88
|
*
|
|
89
|
+
* Sentinel-aware: when `passthrough` contains a `--` end-of-options
|
|
90
|
+
* sentinel and no existing --append-system-prompt match is found, the new
|
|
91
|
+
* `--append-system-prompt <joined>` pair is inserted BEFORE the sentinel
|
|
92
|
+
* so claude parses it as a flag, not as additional prompt text. Without
|
|
93
|
+
* this, `fnc -- "say hi"` produced `claude … -- say hi --append-system-prompt
|
|
94
|
+
* <fragments>` and claude swallowed the whole tail as prompt content.
|
|
95
|
+
*
|
|
89
96
|
* Never mutates the input slice.
|
|
90
97
|
*/
|
|
91
98
|
export function withAppendedSystemPrompts(
|
|
@@ -110,6 +117,15 @@ export function withAppendedSystemPrompts(
|
|
|
110
117
|
return out;
|
|
111
118
|
}
|
|
112
119
|
}
|
|
120
|
+
const sentinelAt = passthrough.indexOf('--');
|
|
121
|
+
if (sentinelAt >= 0) {
|
|
122
|
+
return [
|
|
123
|
+
...passthrough.slice(0, sentinelAt),
|
|
124
|
+
'--append-system-prompt',
|
|
125
|
+
joined,
|
|
126
|
+
...passthrough.slice(sentinelAt),
|
|
127
|
+
];
|
|
128
|
+
}
|
|
113
129
|
return [...passthrough, '--append-system-prompt', joined];
|
|
114
130
|
}
|
|
115
131
|
|