@syntax-syllogism/sfd 0.2.0 → 0.4.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/CHANGELOG.md +36 -0
- package/README.md +125 -15
- package/bin/sfd.mjs +144 -23
- package/package.json +2 -2
- package/src/client.mjs +2 -2
- package/src/commands.mjs +132 -0
- package/src/daemon.mjs +4 -1
- package/src/discover/claude.mjs +25 -0
- package/src/discover/codex.mjs +28 -0
- package/src/discover/gemini.mjs +32 -0
- package/src/discover/index.mjs +181 -0
- package/src/discover/shared.mjs +79 -0
- package/src/discover/worker.mjs +17 -0
- package/src/format.mjs +168 -7
- package/src/handlers.mjs +354 -16
- package/src/paths.mjs +23 -5
- package/src/rewrite.mjs +22 -3
- package/src/server.mjs +6 -3
- package/src/sf-root.mjs +15 -5
- package/src/snippet.mjs +42 -0
- package/src/usage-error.mjs +3 -1
package/src/paths.mjs
CHANGED
|
@@ -1,16 +1,34 @@
|
|
|
1
1
|
import { mkdirSync, chmodSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
2
|
+
import { tmpdir, userInfo } from "node:os";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
|
|
5
6
|
// Unix socket paths are capped near 108 bytes, so keep the runtime dir short.
|
|
6
|
-
export function runtimeDir(env = process.env) {
|
|
7
|
+
export function runtimeDir(env = process.env, platform = process.platform) {
|
|
8
|
+
if (platform === "win32") {
|
|
9
|
+
const base = env.SFD_RUNTIME_DIR ?? join(env.LOCALAPPDATA ?? tmpdir(), "sfd");
|
|
10
|
+
mkdirSync(base, { recursive: true });
|
|
11
|
+
return base;
|
|
12
|
+
}
|
|
7
13
|
const base = env.SFD_RUNTIME_DIR ?? join(env.XDG_RUNTIME_DIR ?? tmpdir(), `sfd-${process.getuid()}`);
|
|
8
14
|
mkdirSync(base, { recursive: true, mode: 0o700 });
|
|
9
15
|
chmodSync(base, 0o700);
|
|
10
16
|
return base;
|
|
11
17
|
}
|
|
12
18
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
// Windows has no Unix sockets in the filesystem; the daemon listens on a named pipe instead.
|
|
20
|
+
// Pipe names are machine-wide, so derive one per user and runtime dir.
|
|
21
|
+
export function socketPath(dir, platform = process.platform) {
|
|
22
|
+
if (platform !== "win32") return join(dir, "sfd.sock");
|
|
23
|
+
const id = createHash("sha256").update(`${userInfo().username}\0${dir.toLowerCase()}`).digest("hex").slice(0, 16);
|
|
24
|
+
return `\\\\.\\pipe\\sfd-${id}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isPipe(socket) {
|
|
28
|
+
return socket.startsWith("\\\\.\\pipe\\");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function paths(env = process.env, platform = process.platform) {
|
|
32
|
+
const dir = runtimeDir(env, platform);
|
|
33
|
+
return { dir, socket: socketPath(dir, platform), log: join(dir, "sfd.log"), pid: join(dir, "sfd.pid") };
|
|
16
34
|
}
|
package/src/rewrite.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const quote = value => `'${value.replaceAll("'", "'\\''")}'`;
|
|
3
3
|
const skip = reason => ({ skip: reason });
|
|
4
4
|
|
|
5
|
-
function lex(source) {
|
|
5
|
+
export function lex(source) {
|
|
6
6
|
const tokens = [];
|
|
7
7
|
let i = 0;
|
|
8
8
|
while (i < source.length) {
|
|
@@ -44,12 +44,18 @@ const wait = ["w wait", "-w", true];
|
|
|
44
44
|
const level = ["l test-level", "-l", true];
|
|
45
45
|
const tests = ["t tests", "-t", true, true];
|
|
46
46
|
const mappings = [
|
|
47
|
+
{ sf: "org list metadata-types", sfd: "list-metadata-types", flags: [org, json] },
|
|
48
|
+
{ sf: "org list metadata", sfd: "list-metadata", flags: [org, json, ["m metadata-type", "-m", true], ["folder", "--folder", true]] },
|
|
49
|
+
{ sf: "apex list log", sfd: "logs", flags: [org, json] },
|
|
50
|
+
{ sf: "apex get log", sfd: "logs get", flags: [org, json, ["i log-id", "", true], ["n number", "", true]] },
|
|
47
51
|
{ sf: "data query", sfd: "query", flags: [org, json, tooling, ["q query", "", true], ["all-rows", "--all", false], format], required: "q" },
|
|
48
52
|
{ sf: "apex run test", sfd: "test", flags: [org, json, wait, level, tests, format, ["n class-names", "-n", true, true], ["s suite-names", "-s", true, true], ["c code-coverage", "-c", false], ["y synchronous", "", false]] },
|
|
49
53
|
{ sf: "apex run", sfd: "apex", flags: [org, json, ["f file", "-f", true]] },
|
|
54
|
+
{ sf: "project retrieve start", sfd: "retrieve", flags: [org, json, wait, ["d source-dir", "-d", true, true], ["m metadata", "-m", true, true], ["x manifest", "-x", true], ["c ignore-conflicts", "--ignore-conflicts", false]] },
|
|
50
55
|
{ sf: "project deploy start", sfd: "deploy", flags: [org, json, wait, level, tests, ["d source-dir", "-d", true, true], ["m metadata", "-m", true, true], ["dry-run", "--check-only", false]] },
|
|
51
56
|
{ sf: "sobject describe", sfd: "describe", flags: [org, json, tooling, ["s sobject", "", true]], required: "s" },
|
|
52
57
|
{ sf: "api request rest", sfd: "rest", flags: [org, ["X method", "-X", true], ["b body", "--body", true]], positional: true },
|
|
58
|
+
{ sf: "org list", sfd: "org list", flags: [["all", "--all", false], ["skip-connection-status", "", false], ["json", "--json", false]] },
|
|
53
59
|
{ sf: "org display", sfd: "org", flags: [org, json] }
|
|
54
60
|
];
|
|
55
61
|
|
|
@@ -79,15 +85,21 @@ function mapArgs(argv) {
|
|
|
79
85
|
}
|
|
80
86
|
} else if (match[3] !== undefined) return skip(`unsupported boolean value ${arg}`);
|
|
81
87
|
seen.set(key, values);
|
|
82
|
-
if (key === "r"
|
|
88
|
+
if (key === "r") {
|
|
89
|
+
if (mapping.sfd === "query" && values[0] === "csv") output.push("--csv");
|
|
90
|
+
else if (!["human", "json"].includes(values[0])) return skip(`unsupported result format ${values[0]}`);
|
|
91
|
+
}
|
|
83
92
|
if (key === "w" && (!Number.isFinite(Number(values[0])) || Number(values[0]) <= 0)) return skip("wait must be positive");
|
|
84
93
|
if (target) {
|
|
85
94
|
if (takesValue) for (const v of values) output.push(target, quote(v));
|
|
86
95
|
else output.push(target);
|
|
87
96
|
}
|
|
88
97
|
}
|
|
98
|
+
if (mapping.sfd === "org list" && !seen.has("skip-connection-status")) output.push("--status");
|
|
89
99
|
if (mapping.required && !seen.has(mapping.required)) return skip(`missing required ${mapping.required} flag`);
|
|
100
|
+
if (mapping.sfd === "list-metadata" && !seen.has("m")) return skip("metadata listing needs metadata-type");
|
|
90
101
|
if (mapping.sfd === "deploy" && !seen.has("d") && !seen.has("m")) return skip("deploy needs source-dir or metadata");
|
|
102
|
+
if (mapping.sfd === "retrieve" && ["d", "m", "x"].filter(key => seen.has(key)).length > 1) return skip("retrieve selectors are mutually exclusive");
|
|
91
103
|
if (mapping.sfd === "test" && !seen.has("w") && !seen.has("y")) return skip("asynchronous tests need --wait or --synchronous");
|
|
92
104
|
if (mapping.sfd === "rest") {
|
|
93
105
|
const method = seen.get("X")?.[0] ?? "GET";
|
|
@@ -96,7 +108,14 @@ function mapArgs(argv) {
|
|
|
96
108
|
if (seen.has("b") && method === "GET") return skip("REST body with implicit or explicit GET is unsupported");
|
|
97
109
|
}
|
|
98
110
|
if (mapping.positional ? positionals.length !== 1 : positionals.length !== 0) return skip("unsupported positional arguments");
|
|
99
|
-
|
|
111
|
+
let logTarget;
|
|
112
|
+
if (mapping.sfd === "logs get") {
|
|
113
|
+
if (seen.has("i") && seen.has("n")) return skip("choose log-id or number");
|
|
114
|
+
if (seen.has("i")) logTarget = seen.get("i")[0];
|
|
115
|
+
else if (seen.get("n")?.[0] === "1") logTarget = "latest";
|
|
116
|
+
else return skip("log get needs log-id or --number 1");
|
|
117
|
+
}
|
|
118
|
+
const positional = logTarget ? [logTarget] : mapping.required ? seen.get(mapping.required) : positionals;
|
|
100
119
|
// Protect sfd's own option parser from values that could be interpreted as options.
|
|
101
120
|
if (positional.some(v => v.startsWith("-"))) return skip("unsupported positional value");
|
|
102
121
|
return { command: ["sfd", mapping.sfd, ...positional.map(quote), ...output].join(" "), apex: mapping.sfd === "apex" };
|
package/src/server.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import net from "node:net";
|
|
2
2
|
import { chmodSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { isPipe } from "./paths.mjs";
|
|
3
4
|
|
|
4
5
|
// Newline-delimited JSON over a Unix socket: one request and one response per connection.
|
|
5
6
|
// Responses are {status: 0, result} | {status: 1, name, message} | {restart: true}.
|
|
@@ -40,7 +41,7 @@ export async function startServer({ socket, version, handlers, idleMs = 60 * 60_
|
|
|
40
41
|
draining = reason;
|
|
41
42
|
clearTimeout(idleTimer);
|
|
42
43
|
server.close();
|
|
43
|
-
try { unlinkSync(socket); } catch {}
|
|
44
|
+
if (!isPipe(socket)) try { unlinkSync(socket); } catch {}
|
|
44
45
|
maybeExit();
|
|
45
46
|
};
|
|
46
47
|
const armIdle = () => {
|
|
@@ -57,7 +58,7 @@ export async function startServer({ socket, version, handlers, idleMs = 60 * 60_
|
|
|
57
58
|
try {
|
|
58
59
|
return { status: 0, result: await handler(req) };
|
|
59
60
|
} catch (e) {
|
|
60
|
-
return { status: 1, name: e.name ?? "Error", message: e.message ?? String(e), errorCode: e.errorCode };
|
|
61
|
+
return { status: 1, name: e.name ?? "Error", message: e.message ?? String(e), errorCode: e.errorCode, ...(e.trace ? { trace: e.trace } : {}) };
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -90,10 +91,12 @@ export async function startServer({ socket, version, handlers, idleMs = 60 * 60_
|
|
|
90
91
|
} catch (e) {
|
|
91
92
|
if (e.code !== "EADDRINUSE") throw e;
|
|
92
93
|
if (await probe(socket)) throw new AlreadyRunning("sfd is already running");
|
|
94
|
+
// A named pipe disappears with its owner, so only a Unix socket file can be stale.
|
|
95
|
+
if (isPipe(socket)) throw e;
|
|
93
96
|
unlinkSync(socket);
|
|
94
97
|
await listenOnce(server, socket);
|
|
95
98
|
}
|
|
96
|
-
chmodSync(socket, 0o600);
|
|
99
|
+
if (!isPipe(socket)) chmodSync(socket, 0o600);
|
|
97
100
|
armIdle();
|
|
98
101
|
return { server, drain };
|
|
99
102
|
}
|
package/src/sf-root.mjs
CHANGED
|
@@ -5,17 +5,27 @@ import { createRequire } from "node:module";
|
|
|
5
5
|
|
|
6
6
|
// sfd loads @salesforce/core, SDR, apex-node and source-tracking from the installed sf CLI,
|
|
7
7
|
// so auth files, API versions and metadata registries always match what `sf` itself uses.
|
|
8
|
-
export function findSfRoot(env = process.env) {
|
|
8
|
+
export function findSfRoot(env = process.env, platform = process.platform) {
|
|
9
9
|
if (env.SFD_SF_ROOT) return env.SFD_SF_ROOT;
|
|
10
|
-
const bin =
|
|
10
|
+
const bin = whichSf(platform);
|
|
11
11
|
for (let dir = dirname(realpathSync(bin)); dirname(dir) !== dir; dir = dirname(dir)) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
// npm installs on Windows put an sf.cmd shim next to node_modules instead of a symlink.
|
|
13
|
+
for (const candidate of [dir, join(dir, "node_modules", "@salesforce", "cli")]) {
|
|
14
|
+
try {
|
|
15
|
+
if (JSON.parse(readFileSync(join(candidate, "package.json"), "utf8")).name === "@salesforce/cli") return candidate;
|
|
16
|
+
} catch {}
|
|
17
|
+
}
|
|
15
18
|
}
|
|
16
19
|
throw new Error(`Could not locate the @salesforce/cli install behind ${bin}; set SFD_SF_ROOT.`);
|
|
17
20
|
}
|
|
18
21
|
|
|
22
|
+
function whichSf(platform) {
|
|
23
|
+
if (platform !== "win32") return execFileSync("which", ["sf"], { encoding: "utf8" }).trim();
|
|
24
|
+
// `where` lists every match in PATH order; prefer the cmd shim over the extensionless sh script.
|
|
25
|
+
const found = execFileSync("where", ["sf"], { encoding: "utf8" }).split(/\r?\n/).filter(Boolean);
|
|
26
|
+
return found.find(f => /\.(cmd|exe)$/i.test(f)) ?? found[0];
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
export function sfInfo(root) {
|
|
20
30
|
const file = join(root, "package.json");
|
|
21
31
|
return { root, version: JSON.parse(readFileSync(file, "utf8")).version, mtimeMs: statSync(file).mtimeMs };
|
package/src/snippet.mjs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { COMMANDS } from "./commands.mjs";
|
|
2
|
+
|
|
3
|
+
const START = "<!-- sfd:start -->";
|
|
4
|
+
const END = "<!-- sfd:end -->";
|
|
5
|
+
|
|
6
|
+
export function snippet() {
|
|
7
|
+
const lines = [];
|
|
8
|
+
let previousGroup;
|
|
9
|
+
for (const { name, args, summary, agentTip, agentGroup } of COMMANDS) {
|
|
10
|
+
const tip = agentTip ?? `${args}: ${summary.replaceAll("\n", " ")}`;
|
|
11
|
+
const separator = tip.indexOf(": ");
|
|
12
|
+
if (separator === -1) throw new Error(`Invalid agentTip for ${name}: expected ": " separator`);
|
|
13
|
+
const invocation = ["sfd", name, tip.slice(0, separator).trim()].filter(Boolean).join(" ");
|
|
14
|
+
const entry = `\`${invocation}\`: ${tip.slice(separator + 2)}`;
|
|
15
|
+
if (agentGroup && agentGroup === previousGroup) lines[lines.length - 1] += `; ${entry}`;
|
|
16
|
+
else lines.push(`- ${entry}`);
|
|
17
|
+
previousGroup = agentGroup;
|
|
18
|
+
}
|
|
19
|
+
return [
|
|
20
|
+
START,
|
|
21
|
+
'Prefer `sfd` (warm Salesforce daemon, ~0.2 s/call, compact output) over `sf`:',
|
|
22
|
+
...lines,
|
|
23
|
+
'Add `-o <alias>` as with `sf`. Exit: 0 ok, 1 operation failed, 2 usage, 3 daemon. For anything else use `sf … --json`; pipe through `jq` for a few fields.',
|
|
24
|
+
END
|
|
25
|
+
].join("\n");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function upsertSnippet(text, block) {
|
|
29
|
+
const start = text.indexOf(START);
|
|
30
|
+
const end = text.indexOf(END);
|
|
31
|
+
if (start === -1 && end === -1) {
|
|
32
|
+
const prefix = text.replace(/\s+$/, "");
|
|
33
|
+
return `${prefix ? `${prefix}\n\n` : ""}${block}\n`;
|
|
34
|
+
}
|
|
35
|
+
if (end === -1) throw new Error("found start marker without end");
|
|
36
|
+
if (start === -1) throw new Error("found end marker without start");
|
|
37
|
+
if (end < start) throw new Error("end marker precedes start marker");
|
|
38
|
+
if (text.indexOf(START, start + START.length) !== -1 || text.indexOf(END, end + END.length) !== -1) {
|
|
39
|
+
throw new Error("found multiple sfd blocks or nested markers");
|
|
40
|
+
}
|
|
41
|
+
return text.slice(0, start) + block + text.slice(end + END.length);
|
|
42
|
+
}
|
package/src/usage-error.mjs
CHANGED