sfbench 0.1.0 → 0.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/README.ja.md +9 -1
- package/README.md +8 -1
- package/dist/server/cli.js +8 -0
- package/dist/server/launcher.js +61 -14
- package/dist/server/nodeVersion.js +27 -0
- package/dist/server/serve.js +31 -4
- package/dist/web/assets/{index-CW8D9pEg.js → index-BIIHMLxB.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -51,7 +51,7 @@ lwc-soql-builder は、Salesforce が OAuth のユーザーエージェントフ
|
|
|
51
51
|
|
|
52
52
|
## 必要なもの
|
|
53
53
|
|
|
54
|
-
- Node.js 22.21.0 以上の 22 系、または 24.5.0
|
|
54
|
+
- Node.js 22.21.0 以上の 22 系、または 24.5.0 以上(プロジェクトで古い Node.js を指定していても、`npx -y -p node@24 -p sfbench -- sfbench` で動かせます)
|
|
55
55
|
- [Salesforce CLI](https://developer.salesforce.com/tools/salesforcecli)(`sf`)と、使う組織へのログイン
|
|
56
56
|
|
|
57
57
|
macOS、Linux、Windows で動きます。
|
|
@@ -108,6 +108,14 @@ Claude Code の中では `/plugin install sfbench --marketplace atskimura/sfbenc
|
|
|
108
108
|
日本語版は [plugins/sfbench/ja/sfbench/SKILL.md](plugins/sfbench/ja/sfbench/SKILL.md)、英語版は [plugins/sfbench/skills/sfbench/SKILL.md](plugins/sfbench/skills/sfbench/SKILL.md) です。
|
|
109
109
|
`sfbench` フォルダごと、エージェントがスキルを読む場所(Claude Code なら `~/.claude/skills/`、Codex なら `~/.agents/skills/`)に置きます(Codex では試していません)。
|
|
110
110
|
|
|
111
|
+
Claude Code のシェルのサンドボックスを有効にしている場合(`/sandbox`)、sfbench はサンドボックスの中では起動できません。
|
|
112
|
+
`~/.npm/` と `~/.sfbench/` に書き込み、ローカルのサーバーを動かし続けるからです。
|
|
113
|
+
`~/.claude/settings.json` に次を足し、sfbench だけをサンドボックスの外で動かしてください。
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
"sandbox": { "excludedCommands": ["npx -y sfbench *", "npx -y sfbench", "npx -y -p node@24 -p sfbench -- sfbench *", "npx -y -p node@24 -p sfbench -- sfbench"] }
|
|
117
|
+
```
|
|
118
|
+
|
|
111
119
|
スキルを入れた Claude Code に「sfbench で成約済みの商談を一覧して」と頼むと、エージェントは SOQL を組み立てて次のように起動します。
|
|
112
120
|
|
|
113
121
|
```
|
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ lwc-soql-builder can no longer log in, because Salesforce has blocked the OAuth
|
|
|
51
51
|
|
|
52
52
|
## Requirements
|
|
53
53
|
|
|
54
|
-
- Node.js 22.21.0 or later in the 22 line, or 24.5.0 or later
|
|
54
|
+
- Node.js 22.21.0 or later in the 22 line, or 24.5.0 or later (if a project pins an older Node.js, `npx -y -p node@24 -p sfbench -- sfbench` still runs it)
|
|
55
55
|
- The [Salesforce CLI](https://developer.salesforce.com/tools/salesforcecli) (`sf`), logged in to the orgs you use
|
|
56
56
|
|
|
57
57
|
sfbench runs on macOS, Linux and Windows.
|
|
@@ -107,6 +107,13 @@ Your instructions file (`CLAUDE.md`) stays as it is.
|
|
|
107
107
|
You can also put the skill's file in place yourself: [plugins/sfbench/skills/sfbench/SKILL.md](plugins/sfbench/skills/sfbench/SKILL.md) in English, or [plugins/sfbench/ja/sfbench/SKILL.md](plugins/sfbench/ja/sfbench/SKILL.md) in Japanese.
|
|
108
108
|
Copy the `sfbench` folder to where the agent reads skills (`~/.claude/skills/` for Claude Code, `~/.agents/skills/` for Codex; untested with Codex).
|
|
109
109
|
|
|
110
|
+
If you turned on Claude Code's command sandbox (`/sandbox`), sfbench cannot start inside it: it writes `~/.npm/` and `~/.sfbench/` and keeps a local server running.
|
|
111
|
+
Let sfbench alone run outside the sandbox by adding this to `~/.claude/settings.json`:
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
"sandbox": { "excludedCommands": ["npx -y sfbench *", "npx -y sfbench", "npx -y -p node@24 -p sfbench -- sfbench *", "npx -y -p node@24 -p sfbench -- sfbench"] }
|
|
115
|
+
```
|
|
116
|
+
|
|
110
117
|
With the skill, ask Claude Code to "list the won opportunities in sfbench", and it writes the SOQL and starts sfbench like this:
|
|
111
118
|
|
|
112
119
|
```
|
package/dist/server/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import { readFileSync } from 'node:fs';
|
|
|
5
5
|
import { openSfbench, stopSfbench, UserError } from "./launcher.js";
|
|
6
6
|
import { restLaunch } from "./rest.js";
|
|
7
7
|
import { MAX_APEX_CODE } from "../shared/api.js";
|
|
8
|
+
import { unsupportedNode } from "./nodeVersion.js";
|
|
8
9
|
import { serve } from "./serve.js";
|
|
9
10
|
import { packageVersion } from "./version.js";
|
|
10
11
|
const HELP = `Usage: sfbench [options]
|
|
@@ -40,6 +41,13 @@ Examples for AI agents:
|
|
|
40
41
|
npx -y sfbench -o my-org --apex-file fix-data.apex
|
|
41
42
|
`;
|
|
42
43
|
async function main() {
|
|
44
|
+
// First, before the arguments: an older Node.js may not parse them (--no-open).
|
|
45
|
+
// Help, the version and stop start no server, so they work on any.
|
|
46
|
+
const args = process.argv.slice(2);
|
|
47
|
+
const startsNoServer = args[0] === 'stop' || args.some((a) => ['-h', '--help', '-v', '--version'].includes(a));
|
|
48
|
+
const tooOld = startsNoServer ? undefined : unsupportedNode();
|
|
49
|
+
if (tooOld)
|
|
50
|
+
throw new UserError(tooOld);
|
|
43
51
|
const { values, positionals } = parseArgs({
|
|
44
52
|
allowPositionals: true,
|
|
45
53
|
allowNegative: true,
|
package/dist/server/launcher.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// The CLI entry point's work: find or start the background server, then open
|
|
2
2
|
// a new browser tab. See docs/design.md ("起動").
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
|
+
import { closeSync, existsSync, openSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
4
6
|
import { fileURLToPath } from 'node:url';
|
|
5
7
|
import { openBrowser } from "./browser.js";
|
|
6
8
|
import { ensureDirs, resolvePaths } from "./paths.js";
|
|
@@ -57,19 +59,61 @@ async function stopServer(info) {
|
|
|
57
59
|
}
|
|
58
60
|
return false;
|
|
59
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Opens a file for the server's stderr, one per start, so that a server still
|
|
64
|
+
* running from before keeps its own. The server removes it once it listens
|
|
65
|
+
* (serve.ts): it is kept only to tell why a start failed. Undefined when it
|
|
66
|
+
* cannot be opened; the server then starts with its stderr ignored.
|
|
67
|
+
*/
|
|
68
|
+
function openStderrFile(paths) {
|
|
69
|
+
const file = join(paths.tmpDir, `server-stderr-${Date.now()}-${process.pid}.log`);
|
|
70
|
+
try {
|
|
71
|
+
return { file, fd: openSync(file, 'w', 0o600) };
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
60
77
|
function startServer(paths) {
|
|
61
78
|
// cli.js next to this file when installed; cli.ts when run from the sources.
|
|
62
79
|
const cli = fileURLToPath(new URL(import.meta.url.endsWith('.ts') ? './cli.ts' : './cli.js', import.meta.url));
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
// Node.js refusing to start the server (an unknown option, a missing module)
|
|
81
|
+
// says so on stderr, before the server can log anything.
|
|
82
|
+
const stderr = openStderrFile(paths);
|
|
83
|
+
let ended = false;
|
|
84
|
+
try {
|
|
85
|
+
const child = spawn(process.execPath, ['--use-env-proxy', cli, 'serve'], {
|
|
86
|
+
cwd: paths.tmpDir,
|
|
87
|
+
detached: true,
|
|
88
|
+
stdio: ['ignore', 'ignore', stderr ? stderr.fd : 'ignore'],
|
|
89
|
+
env: { ...process.env, ...(stderr ? { SFBENCH_STDERR_FILE: stderr.file } : {}) },
|
|
90
|
+
windowsHide: true,
|
|
91
|
+
});
|
|
92
|
+
child.on('exit', () => (ended = true));
|
|
93
|
+
child.on('error', () => (ended = true));
|
|
94
|
+
child.unref();
|
|
95
|
+
if (child.pid === undefined)
|
|
96
|
+
throw new UserError('Could not start the sfbench server.');
|
|
97
|
+
return {
|
|
98
|
+
pid: child.pid,
|
|
99
|
+
ended: () => ended,
|
|
100
|
+
stderr: () => {
|
|
101
|
+
if (!stderr)
|
|
102
|
+
return '';
|
|
103
|
+
try {
|
|
104
|
+
return readFileSync(stderr.file, 'utf8').trim().slice(0, 2000);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return '';
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
stderrFile: stderr?.file,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
if (stderr)
|
|
115
|
+
closeSync(stderr.fd);
|
|
116
|
+
}
|
|
73
117
|
}
|
|
74
118
|
/** Returns a running server of this version or newer, starting one if needed. Call with server.lock held. */
|
|
75
119
|
async function ensureServer(paths) {
|
|
@@ -87,15 +131,18 @@ async function ensureServer(paths) {
|
|
|
87
131
|
}
|
|
88
132
|
}
|
|
89
133
|
}
|
|
90
|
-
const
|
|
134
|
+
const server = startServer(paths);
|
|
91
135
|
const deadline = Date.now() + START_TIMEOUT_MS;
|
|
92
|
-
while (Date.now() < deadline) {
|
|
136
|
+
while (Date.now() < deadline && !server.ended()) {
|
|
93
137
|
await sleep(200);
|
|
94
138
|
const info = readServerInfo(paths.serverJson);
|
|
95
|
-
if (info?.pid === pid && (await health(info)) !== undefined)
|
|
139
|
+
if (info?.pid === server.pid && (await health(info)) !== undefined)
|
|
96
140
|
return info;
|
|
97
141
|
}
|
|
98
|
-
|
|
142
|
+
const said = server.stderr();
|
|
143
|
+
// The server removes its stderr file once it listens: named only while it is there.
|
|
144
|
+
const files = [paths.serverLog, ...(server.stderrFile && existsSync(server.stderrFile) ? [server.stderrFile] : [])].join(' and ');
|
|
145
|
+
throw new UserError(`The sfbench server did not start${said ? `:\n${said}` : '.'}\nSee ${files}.`);
|
|
99
146
|
}
|
|
100
147
|
async function resolveOrg(options) {
|
|
101
148
|
if (options.targetOrg)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Whether this Node.js can run the sfbench server, which is started with
|
|
2
|
+
// --use-env-proxy (docs/design.md "プロキシ"). Node.js has it from 22.21.0 and
|
|
3
|
+
// 24.5.0 (package.json "engines"); an older one refuses the option and the
|
|
4
|
+
// server dies at once. The option itself is checked, not the version.
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { packageRoot } from "./paths.js";
|
|
8
|
+
/** The versions package.json "engines" asks for, as the message names them. */
|
|
9
|
+
function engines() {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8')).engines?.node ?? 'a newer one';
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return 'a newer one';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Why this Node.js cannot run sfbench, with what to do; undefined when it can. */
|
|
18
|
+
export function unsupportedNode(hasEnvProxy = process.allowedNodeEnvironmentFlags.has('--use-env-proxy'), version = process.versions.node, execPath = process.execPath, wanted = engines()) {
|
|
19
|
+
if (hasEnvProxy)
|
|
20
|
+
return undefined;
|
|
21
|
+
return [
|
|
22
|
+
`sfbench needs Node.js ${wanted}, but this is Node.js ${version} (${execPath}).`,
|
|
23
|
+
'A version manager (asdf, nvm, mise and the like) may pick an older Node.js in this folder.',
|
|
24
|
+
'To run sfbench with Node.js 24 from npm anyway:',
|
|
25
|
+
' npx -y -p node@24 -p sfbench -- sfbench <options>',
|
|
26
|
+
].join('\n');
|
|
27
|
+
}
|
package/dist/server/serve.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// the background; `npm run dev:server` runs it in the foreground.
|
|
3
3
|
import { randomBytes } from 'node:crypto';
|
|
4
4
|
import { readdirSync, rmSync, statSync } from 'node:fs';
|
|
5
|
-
import { join } from 'node:path';
|
|
5
|
+
import { basename, dirname, join } from 'node:path';
|
|
6
6
|
import { readJson, writeFileAtomic } from "./fsutil.js";
|
|
7
7
|
import { App } from "./http/server.js";
|
|
8
8
|
import { initLog, log } from "./log.js";
|
|
@@ -52,9 +52,35 @@ function cleanTmp(dir) {
|
|
|
52
52
|
const now = Date.now();
|
|
53
53
|
for (const name of readdirSync(dir)) {
|
|
54
54
|
const file = join(dir, name);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
try {
|
|
56
|
+
const mtime = statSync(file, { throwIfNoEntry: false })?.mtimeMs;
|
|
57
|
+
if (mtime !== undefined && now - mtime > TMP_MAX_AGE_MS)
|
|
58
|
+
rmSync(file, { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// In use (Windows refuses to remove a file a running server keeps open): left for the next time.
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Whether a path is one of the files the CLI keeps a server's stderr in: only such a file is removed. */
|
|
66
|
+
export function isStderrFile(file, tmpDir) {
|
|
67
|
+
return dirname(file) === tmpDir && /^server-stderr-[\w-]+\.log$/.test(basename(file));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Removes the file the CLI kept this server's stderr in, once it listens: it
|
|
71
|
+
* was only to tell why a start failed, and must not keep growing while the
|
|
72
|
+
* server runs (launcher.ts). Only a file of that name in tmp/ is removed.
|
|
73
|
+
* Windows may refuse to remove an open file; it is then cleared with tmp/.
|
|
74
|
+
*/
|
|
75
|
+
function removeStderrFile(paths) {
|
|
76
|
+
const file = process.env.SFBENCH_STDERR_FILE;
|
|
77
|
+
if (!file || !isStderrFile(file, paths.tmpDir))
|
|
78
|
+
return;
|
|
79
|
+
try {
|
|
80
|
+
rmSync(file, { force: true });
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Kept until tmp/ is cleaned.
|
|
58
84
|
}
|
|
59
85
|
}
|
|
60
86
|
export async function serve(dev) {
|
|
@@ -100,6 +126,7 @@ export async function serve(dev) {
|
|
|
100
126
|
// never sees a server.json for a server that is not ready.
|
|
101
127
|
writeServerInfo(paths.serverJson, { port, adminToken, pid: process.pid, version });
|
|
102
128
|
log.info(`sfbench ${version} listening on 127.0.0.1:${port}${dev ? ' (dev)' : ''}`);
|
|
129
|
+
removeStderrFile(paths);
|
|
103
130
|
break;
|
|
104
131
|
}
|
|
105
132
|
}
|