flowsprite 0.1.0 → 0.1.4
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 +8 -5
- package/dist/bin.js +45 -45
- package/dist/cli.js +615 -131
- package/dist/credentials.js +108 -0
- package/dist/doctor.js +17 -3
- package/dist/ghLogin.js +180 -0
- package/dist/login.js +290 -37
- package/dist/mcpClient.js +39 -1
- package/dist/messages.js +147 -0
- package/dist/output.js +55 -3
- package/dist/redact.js +51 -23
- package/dist/setup.js +282 -17
- package/dist/sfCli.js +179 -0
- package/dist/status.js +11 -1
- package/dist/usage.js +65 -25
- package/dist/version.js +1 -1
- package/dist/wiring.js +96 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -21,13 +21,13 @@ Flowsprite repo). To run an unreleased change from a checkout of this repo:
|
|
|
21
21
|
|
|
22
22
|
| command | what it does |
|
|
23
23
|
| --- | --- |
|
|
24
|
-
| `setup` (default) | Runs the whole ladder: log in if needed, then connect Salesforce, connect GitHub,
|
|
24
|
+
| `setup` (default) | Runs the whole ladder: log in if needed, then connect Salesforce, connect GitHub, give each connection a repository and start the first sync, in the order the server asks for them. It needs **no flags** — where the workspace leaves a choice (which connection, which installation) it asks at a terminal and, through a pipe, lists the options and stops rather than guessing. At the repository step it offers to create a new private repository with a suggested name (Enter twice accepts), waits for GitHub to finish creating it, then runs the first sync. |
|
|
25
25
|
| `login` | Device-code login. Prints a code and a URL; approve it in a browser. `--token <t>` skips the loop and uses that token instead (add `--save` to store it). |
|
|
26
26
|
| `logout` | Removes this endpoint's entry from the credentials file. Other endpoints keep theirs. |
|
|
27
27
|
| `status` | The setup checklist, the Salesforce connections, the GitHub installations and the next step. `--json` prints the tool's payload unmodified. |
|
|
28
|
-
| `connect salesforce` | Mints a one-time browser link (`--sandbox`, `--my-domain <host>`), prints it, and
|
|
28
|
+
| `connect salesforce` | Mints a one-time browser link (`--sandbox`, `--my-domain <host>` — both also work on `setup`), prints it, says if an org is already connected, and waits until a NEW connection lands. |
|
|
29
29
|
| `connect github` | The same for installing the GitHub app. |
|
|
30
|
-
| `bind` |
|
|
30
|
+
| `bind` | Gives a Salesforce connection a repo. With no flags it offers the connections and installations `setup_status` lists, then asks whether to create a new private repository (with a suggested name) — answer no to pick one of the existing repositories it lists instead. Through a pipe it never creates without being told: `--create <name>` creates one, `--repo owner/name` binds an existing one, and `--connection <id>` / `--installation <n>` pin the rest. `--create` always makes a **private** repo — Flowsprite refuses to mirror into a public one. A personal GitHub account needs a one-time sign-in before Flowsprite can create a repository on it; the command prints the link. |
|
|
31
31
|
| `sync` | `sfdc_trigger_sync` then `sfdc_sync_logs` for `--connection <id>`. |
|
|
32
32
|
| `doctor` | node version, endpoint reachability, whether a credential exists for this endpoint (never its value), and whether `sf` and `gh` are on PATH. |
|
|
33
33
|
| `mcp install` | Points an AI client at this workspace's MCP endpoint. `--client claude-code\|codex\|claude-desktop\|cursor`; with no `--client` it offers the clients it detects and names the rest as print-only. Runs the client's own registration command where there is one (`claude mcp add`, `codex mcp add`), and otherwise writes the client's config file — backed up once, one entry changed, `0600`. `--print` shows what it would do and changes nothing; `--force` replaces an existing `flowsprite` entry (at a terminal it asks instead); `--token-env NAME` tells codex which variable holds the bearer. |
|
|
@@ -62,9 +62,12 @@ again.
|
|
|
62
62
|
**One command prints a token, and only at a terminal: `mcp print --reveal`.**
|
|
63
63
|
Everything else — every success line, every refusal, a failing client command's
|
|
64
64
|
own stderr — carries `<YOUR_TOKEN>` or nothing, and no line ever carries a command
|
|
65
|
-
line. Every line leaves this process through one writer that rewrites any
|
|
65
|
+
line. Every other line leaves this process through one writer that rewrites any
|
|
66
66
|
`fp_user_`/`fp_live_` value to its prefix plus the last four characters, so a
|
|
67
|
-
token cannot reach a scrollback even from an error string that quoted one.
|
|
67
|
+
token cannot reach a scrollback even from an error string that quoted one. The
|
|
68
|
+
`--reveal` lines are the one exception, and they exist only when stdout is a
|
|
69
|
+
terminal: through a pipe the command refuses with exit 2 — no token, no reveal
|
|
70
|
+
line; the refusal goes to stderr.
|
|
68
71
|
|
|
69
72
|
## What `mcp install` touches
|
|
70
73
|
|
package/dist/bin.js
CHANGED
|
@@ -5,13 +5,17 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
6
|
import { main } from "./cli.js";
|
|
7
7
|
import { isOnPath } from "./doctor.js";
|
|
8
|
-
import {
|
|
9
|
-
import { redact } from "./redact.js";
|
|
8
|
+
import { settleThenReraise } from "./output.js";
|
|
9
|
+
import { printable, redact } from "./redact.js";
|
|
10
10
|
import { VERSION } from "./version.js";
|
|
11
|
+
import { wireDeps } from "./wiring.js";
|
|
11
12
|
/**
|
|
12
13
|
* THE ONLY FILE THAT NAMES A REAL EFFECT. Everything under it takes its fetch,
|
|
13
14
|
* clock, sleep, environment, writer, PATH probe and prompt as arguments, so the
|
|
14
15
|
* whole CLI is drivable in a test without a network, a home directory or a TTY.
|
|
16
|
+
* The ASSEMBLY of those arguments lives in `wiring.ts` (FLO-513) so a test can
|
|
17
|
+
* run the same wiring with a recording stdout; this file only hands it the real
|
|
18
|
+
* handles — `process.stdout` itself, never a literal standing in for it.
|
|
15
19
|
*/
|
|
16
20
|
/**
|
|
17
21
|
* Best effort, and deliberately fire-and-forget.
|
|
@@ -36,7 +40,6 @@ function openUrl(url) {
|
|
|
36
40
|
// login lost. The URL is already on screen.
|
|
37
41
|
}
|
|
38
42
|
}
|
|
39
|
-
const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
40
43
|
/**
|
|
41
44
|
* Run a client's own registration command.
|
|
42
45
|
*
|
|
@@ -61,10 +64,40 @@ function runCommand(command, args) {
|
|
|
61
64
|
error: result.error,
|
|
62
65
|
};
|
|
63
66
|
}
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
const { deps, statusLine } = wireDeps({
|
|
68
|
+
stdout: process.stdout,
|
|
69
|
+
stdin: process.stdin,
|
|
70
|
+
log: console.log,
|
|
71
|
+
error: console.error,
|
|
72
|
+
env: process.env,
|
|
73
|
+
platform: process.platform,
|
|
74
|
+
home: homedir(),
|
|
75
|
+
version: VERSION,
|
|
76
|
+
nodeVersion: process.version,
|
|
77
|
+
fetch: globalThis.fetch,
|
|
78
|
+
spawn: runCommand,
|
|
79
|
+
isOnPath,
|
|
80
|
+
fileExists: existsSync,
|
|
81
|
+
openUrl,
|
|
82
|
+
// Passed always; the wiring decides whether `deps.ask` exists (a terminal on
|
|
83
|
+
// both stdin and stdout).
|
|
84
|
+
ask: async (question) => {
|
|
85
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
86
|
+
try {
|
|
87
|
+
return await rl.question(question);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
rl.close();
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
now: () => Date.now(),
|
|
94
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
67
95
|
});
|
|
96
|
+
// Terminal only, like the status line itself: a piped run keeps node's default
|
|
97
|
+
// SIGINT behaviour byte-for-byte.
|
|
98
|
+
if (statusLine !== null) {
|
|
99
|
+
process.once("SIGINT", settleThenReraise(statusLine, () => process.kill(process.pid, "SIGINT")));
|
|
100
|
+
}
|
|
68
101
|
/**
|
|
69
102
|
* THE LAST LINE OF DEFENCE: no unexpected throw reaches a person as a stack
|
|
70
103
|
* trace.
|
|
@@ -75,54 +108,21 @@ const writer = createWriter({
|
|
|
75
108
|
* exit code set at all — and it is an uncontrolled OUTPUT PATH: node prints it
|
|
76
109
|
* itself, around the writer, so every redaction this CLI does is bypassed by the
|
|
77
110
|
* one message nobody wrote on purpose. So the message is redacted explicitly
|
|
78
|
-
* here,
|
|
111
|
+
* here, control characters are blanked (`printable` — an error message can
|
|
112
|
+
* quote a path or an env value, and a terminal would execute an ESC/BEL in it),
|
|
113
|
+
* the frames are dropped, and the exit code is 1.
|
|
79
114
|
*
|
|
80
115
|
* Commands do NOT rely on this: every expected failure is a typed result with its
|
|
81
116
|
* own sentence and exit code. This catches what nobody predicted.
|
|
82
117
|
*/
|
|
83
118
|
let rc;
|
|
84
119
|
try {
|
|
85
|
-
rc = await main(process.argv.slice(2),
|
|
86
|
-
env: process.env,
|
|
87
|
-
writer,
|
|
88
|
-
fetch: globalThis.fetch,
|
|
89
|
-
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
90
|
-
now: () => Date.now(),
|
|
91
|
-
nodeVersion: process.version,
|
|
92
|
-
isOnPath,
|
|
93
|
-
version: VERSION,
|
|
94
|
-
spawn: runCommand,
|
|
95
|
-
fileExists: existsSync,
|
|
96
|
-
// `mcp print --reveal` is the ONE command allowed to put a token on stdout,
|
|
97
|
-
// and this is the fact it is allowed on. stdout only: stdin may be a here-doc
|
|
98
|
-
// while the person is still watching the terminal the secret would land in.
|
|
99
|
-
isTTY: process.stdout.isTTY === true,
|
|
100
|
-
home: homedir(),
|
|
101
|
-
platform: process.platform,
|
|
102
|
-
// The browser opener and the prompt are BOTH gated on there being a terminal.
|
|
103
|
-
// Measured 2026-09-15: a non-interactive `login` (stdout piped, as in a CI
|
|
104
|
-
// job or a probe script) still popped a browser tab on this machine, because
|
|
105
|
-
// the opener was wired unconditionally. A process nobody is watching must
|
|
106
|
-
// not take over the screen of whoever happens to be at the machine.
|
|
107
|
-
...(interactive
|
|
108
|
-
? {
|
|
109
|
-
openUrl,
|
|
110
|
-
ask: async (question) => {
|
|
111
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
112
|
-
try {
|
|
113
|
-
return await rl.question(question);
|
|
114
|
-
}
|
|
115
|
-
finally {
|
|
116
|
-
rl.close();
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
}
|
|
120
|
-
: {}),
|
|
121
|
-
});
|
|
120
|
+
rc = await main(process.argv.slice(2), deps);
|
|
122
121
|
}
|
|
123
122
|
catch (err) {
|
|
124
123
|
const message = err instanceof Error ? err.message : String(err);
|
|
125
|
-
|
|
124
|
+
statusLine?.settle();
|
|
125
|
+
console.error(`flowsprite: ${printable(redact(message.split("\n")[0] ?? "unexpected error"))}`);
|
|
126
126
|
rc = 1;
|
|
127
127
|
}
|
|
128
128
|
process.exitCode = rc;
|