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/dist/sfCli.js ADDED
@@ -0,0 +1,179 @@
1
+ /**
2
+ * The pure half of `flowsprite connect salesforce --from-sf-cli <alias>`
3
+ * (FLO-515): the alias rule, the `sf org auth show-sfdx-auth-url --json`
4
+ * envelope parser, the remedies, and the allow-list projection of the door's
5
+ * reply.
6
+ *
7
+ * The command changed on 2026-09-20 (card 263cefa5). FLO-515 read the login
8
+ * from `sf org display --verbose --json`; sf 2.147.0 (published 2026-07-31,
9
+ * `npm view @salesforce/cli time`) bundles @salesforce/plugin-org 6.0.x, which
10
+ * writes a `[REDACTED] Use 'sf org auth show-sfdx-auth-url' to view`
11
+ * placeholder into that command's `sfdxAuthUrl` unless a documented-temporary
12
+ * env var is set, so on a current sf the old parser answered `no_url` for a
13
+ * perfectly good web login (measured on sf 2.150.6, 2026-09-20). The stable
14
+ * command this module now parses ships in the same plugin version and needs no
15
+ * env var. Its `--json` stdout is `{status, result:{sfdxAuthUrl}, warnings}` —
16
+ * no accessToken, no password, no clientId; the command's own warning text
17
+ * lands both in that `warnings` array and on stderr (sf-plugins-core
18
+ * `toSuccessJson`/`warn`, measured against 12.2.6), and this CLI prints
19
+ * neither.
20
+ *
21
+ * NOTHING HERE PRINTS. The `sfdxAuthUrl` this module extracts is a reusable
22
+ * Salesforce credential that the CLI's own redactor cannot see (`redact.ts`
23
+ * covers `fp_user_`/`fp_live_` runs only), so the value's only sinks are the
24
+ * spawn stdout it came from and the `tools/call` arguments — the caller in
25
+ * `cli.ts` (`connectFromSfCli`) holds it in one scope and hands it to exactly
26
+ * one place. The acceptance oracle censuses both files for `fs` / `console`
27
+ * identifiers, which is why this module imports nothing.
28
+ */
29
+ /**
30
+ * An sf alias or username: letters, digits, `. _ @ + -`, not starting with `-`
31
+ * (flag-shaped for the downstream CLI), at most 80 characters. `@` and `+`
32
+ * because `--target-org` takes usernames, which are emails and can be
33
+ * plus-addressed. The value is spawned as ONE argv element (never a shell), so
34
+ * the rule exists for the printed remedies and for the third-party CLI's own
35
+ * parser, not for a shell.
36
+ */
37
+ export const SF_ALIAS_RE = /^(?!-)[A-Za-z0-9._@+-]{1,80}$/;
38
+ export const SF_ALIAS_RULE = "--from-sf-cli wants an sf alias or username: letters, digits, . _ @ + -, not starting with -, at most 80 characters.";
39
+ export const FROM_SF_CLI_EXCLUSIVE = "--from-sf-cli takes the org from the sf CLI; --sandbox and --my-domain belong to the browser link.";
40
+ /**
41
+ * Every `force://` run up to the next whitespace becomes a marker that carries
42
+ * NO scheme, so a scrubbed line can never itself read as an auth URL. Applied
43
+ * to every string this command prints that did not originate in this CLI's own
44
+ * constants — a tool's refusal text, an org's display name.
45
+ */
46
+ export function scrubForceUrls(text) {
47
+ return text.replace(/force:\/\/\S*/g, "[sf auth url removed]");
48
+ }
49
+ const SF_URL_PREFIX = "force://PlatformCLI::";
50
+ const ERROR_NAME_RE = /^[A-Za-z0-9_]{1,64}$/;
51
+ /**
52
+ * The sentence for an `sf` that predates the command (card 263cefa5 DM4). It
53
+ * names no alias: a missing subcommand is a property of the INSTALL, not of
54
+ * the org, so the login line and the logout-then-login remedy are both wrong
55
+ * here — logging in again does not add a subcommand. It also stays true if sf
56
+ * ever removes the command, which is why it ends at the browser road.
57
+ */
58
+ export const SF_TOO_OLD_REMEDY = "Your Salesforce CLI does not have `sf org auth show-sfdx-auth-url` (added in sf 2.147.0, July 2026), " +
59
+ "which this command reads the login from. Update it — sf update, or npm install -g @salesforce/cli — " +
60
+ "then run this again; if it persists on a current sf, connect this org from the browser: flowsprite connect salesforce.";
61
+ /**
62
+ * Read the `sf org auth show-sfdx-auth-url --target-org <alias> --json`
63
+ * envelope: `{status, result:{sfdxAuthUrl}, warnings}`, parsed from the first
64
+ * `{` because a CLI notice can precede the JSON (the setup skill body already
65
+ * parses from the first `{` for the other configurations). Only
66
+ * `result.sfdxAuthUrl` is read: `warnings` carries the command's own "this
67
+ * exposes an SFDX Auth URL" text, which also goes to stderr, and neither is
68
+ * returned or printed.
69
+ *
70
+ * A non-zero envelope `status` reports the envelope's `name` — an error-class
71
+ * identifier such as `NamedOrgNotFoundError` (a logged-out or mistyped alias)
72
+ * or `NoRefreshTokenError` (a JWT / access-token login) — and NEVER its
73
+ * free-text `message`: that text is third-party and can quote the URL
74
+ * (spec-gate codex C2), so it is not even returned.
75
+ *
76
+ * There is no `clientId` in this envelope and no `client` reason: the url's own
77
+ * `force://PlatformCLI::` prefix IS the client check here, and the server door
78
+ * re-checks it. An auth file written before the CLI stored its client id
79
+ * yields `force://:<token>@host` (empty client id), which fails that prefix and
80
+ * is `no_url` like an absent or non-string url.
81
+ */
82
+ export function parseSfAuthUrl(stdout) {
83
+ const start = stdout.indexOf("{");
84
+ if (start < 0)
85
+ return { ok: false, reason: "not_json" };
86
+ let doc;
87
+ try {
88
+ doc = JSON.parse(stdout.slice(start));
89
+ }
90
+ catch {
91
+ return { ok: false, reason: "not_json" };
92
+ }
93
+ if (typeof doc !== "object" || doc === null || Array.isArray(doc))
94
+ return { ok: false, reason: "not_json" };
95
+ const envelope = doc;
96
+ if (envelope.status !== 0) {
97
+ const name = typeof envelope.name === "string" && ERROR_NAME_RE.test(envelope.name) ? envelope.name : "";
98
+ return { ok: false, reason: "status", message: name };
99
+ }
100
+ const result = typeof envelope.result === "object" && envelope.result !== null && !Array.isArray(envelope.result)
101
+ ? envelope.result
102
+ : {};
103
+ const url = result.sfdxAuthUrl;
104
+ if (typeof url !== "string" || !url.startsWith(SF_URL_PREFIX))
105
+ return { ok: false, reason: "no_url" };
106
+ return { ok: true, sfdxAuthUrl: url };
107
+ }
108
+ /** The `sf org login web` line for this alias — sandbox users need the host. */
109
+ export function sfLoginLine(alias) {
110
+ return (`sf org login web --alias ${alias} --instance-url <your sandbox My Domain, ` +
111
+ "for example https://acme--uat.sandbox.my.salesforce.com, or https://test.salesforce.com; " +
112
+ "leave --instance-url off for a production or Developer Edition org (sf's default login host)>");
113
+ }
114
+ /** The logout-then-login remedy for a login this door cannot take over. */
115
+ export function sfRelogin(alias) {
116
+ return `Run: sf org logout --target-org ${alias}, then ${sfLoginLine(alias)}`;
117
+ }
118
+ /**
119
+ * What to print when `sf` is not on PATH: the install line(s) for the platform,
120
+ * the PATH note for a fresh npm install the shell cannot see yet, then the
121
+ * login line. No auto-install (DM3): a global install is the person's decision.
122
+ */
123
+ export function sfRemedy(alias, platform) {
124
+ const lines = ["The Salesforce CLI (sf) is not on PATH, so there is no login to hand over. Install it:", " npm install -g @salesforce/cli"];
125
+ if (platform === "darwin")
126
+ lines.push(" brew install --cask salesforce-cli");
127
+ if (platform === "win32")
128
+ lines.push(" or the Windows installer: https://developer.salesforce.com/tools/salesforcecli");
129
+ if (platform !== "win32") {
130
+ lines.push('If you installed it with npm and this shell still cannot find it: export PATH="$(npm prefix -g)/bin:$PATH"');
131
+ }
132
+ lines.push("Then log in to the org and run this command again:", ` ${sfLoginLine(alias)}`);
133
+ return lines;
134
+ }
135
+ const DOOR_CODES = new Set(["sf_cli_refresh_failed", "sf_cli_identity_failed", "sf_cli_env_unresolved", "sf_cli_save_failed"]);
136
+ const ENV_TYPES = new Set(["production", "sandbox", "developer"]);
137
+ /**
138
+ * Rebuild the door's reply from an ALLOW-LIST — exactly {ok, username, orgName,
139
+ * envType, reconnected} or {ok, code} — so a reply is never serialized
140
+ * wholesale and an extra key (the URL echoed back, anything) is dropped rather
141
+ * than printed. The org-reported strings are scrubbed here, before any sink.
142
+ * `null` is "not the door's shape", which the caller reports as unexpected.
143
+ */
144
+ export function projectDoorResult(doc) {
145
+ if (typeof doc !== "object" || doc === null || Array.isArray(doc))
146
+ return null;
147
+ const d = doc;
148
+ if (d.ok === true) {
149
+ if (typeof d.username !== "string" || typeof d.envType !== "string" || !ENV_TYPES.has(d.envType))
150
+ return null;
151
+ if (typeof d.reconnected !== "boolean")
152
+ return null;
153
+ if (d.orgName !== undefined && typeof d.orgName !== "string")
154
+ return null;
155
+ return {
156
+ ok: true,
157
+ username: scrubForceUrls(d.username),
158
+ ...(d.orgName === undefined ? {} : { orgName: scrubForceUrls(d.orgName) }),
159
+ envType: d.envType,
160
+ reconnected: d.reconnected,
161
+ };
162
+ }
163
+ if (d.ok === false && typeof d.code === "string" && DOOR_CODES.has(d.code))
164
+ return { ok: false, code: d.code };
165
+ return null;
166
+ }
167
+ /** One sentence per refusal code the door can answer with. */
168
+ export function doorRefusalSentence(code, alias) {
169
+ switch (code) {
170
+ case "sf_cli_refresh_failed":
171
+ return `Salesforce refused that login's token. Log in again: ${sfLoginLine(alias)}`;
172
+ case "sf_cli_identity_failed":
173
+ return "Salesforce accepted the token but its identity could not be read. Try again in a minute; if it persists, connect this org from the browser.";
174
+ case "sf_cli_env_unresolved":
175
+ return "Flowsprite could not read whether that org is a sandbox and does not guess production versus sandbox. Try again; if it persists, connect it from the browser.";
176
+ default:
177
+ return "Flowsprite could not save the connection. Run `flowsprite status` and try again.";
178
+ }
179
+ }
package/dist/status.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { printable } from "./redact.js";
1
2
  import { asArray, asObject, planNextStep } from "./setup.js";
2
3
  /**
3
4
  * Render a `setup_status` payload as plain text.
@@ -37,6 +38,15 @@ function expiryText(value) {
37
38
  }
38
39
  return new Date(value).toISOString();
39
40
  }
41
+ /** A connection's repository, with its creation state when it is not ready yet (FLO-511 DM5). */
42
+ function repoText(c) {
43
+ const repo = str(c.repo, "none");
44
+ if (c.bootstrapState === "creating")
45
+ return `${repo} (creating)`;
46
+ if (c.bootstrapState === "failed")
47
+ return `${repo} (failed: ${printable(str(c.bootstrapError, "no reason given"))})`;
48
+ return repo;
49
+ }
40
50
  export function renderStatus(payload) {
41
51
  const root = obj(payload);
42
52
  if (root === null) {
@@ -64,7 +74,7 @@ export function renderStatus(payload) {
64
74
  if (connections.length === 0)
65
75
  lines.push(" none");
66
76
  for (const c of connections) {
67
- lines.push(` ${str(c.id)} ${str(c.displayName)} [${str(c.envType)}] repo: ${str(c.repo, "none")} synced: ${yesNo(c.hasSyncJob)}`);
77
+ lines.push(` ${str(c.id)} ${str(c.displayName)} [${str(c.envType)}] repo: ${repoText(c)} synced: ${yesNo(c.hasSyncJob)}`);
68
78
  }
69
79
  const installations = arr(root.installations);
70
80
  lines.push("", `GitHub installations (${installations.length})`);
package/dist/usage.js CHANGED
@@ -1,44 +1,84 @@
1
- /** The usage text, as data, so `--help` and every exit-2 refusal print the same thing. */
1
+ /**
2
+ * The usage text, as data, so `--help` and every exit-2 refusal print the same thing.
3
+ *
4
+ * Card eecc9d6f N5/N6/N13: what Flowsprite is first, then examples, then each
5
+ * command WITH ITS OWN OPTIONS under it. Every flag `cli.ts` parses appears here
6
+ * (pinned in usage.test.ts against the parser's own option table). `--sandbox`
7
+ * names the generic hosts the server picks (`lib/salesforceOAuthExchange.ts`
8
+ * GENERIC_OAUTH_HOST: sandbox -> test.salesforce.com, prod -> login.salesforce.com).
9
+ */
2
10
  export const USAGE = `flowsprite — set up and drive a Flowsprite workspace from a terminal.
3
11
 
12
+ Flowsprite mirrors a Salesforce org's metadata into a PRIVATE GitHub repository.
13
+ \`flowsprite setup\` takes three steps: log this terminal in, connect Salesforce,
14
+ then connect GitHub and create the private repository.
15
+
4
16
  Usage: flowsprite [command] [options]
5
17
 
6
- Commands:
7
- setup Run the whole setup ladder (the default).
8
- login Log this terminal in with a device code.
18
+ Examples:
19
+ flowsprite setup Log in and set the workspace up, step by step.
20
+ flowsprite setup --sandbox The same, connecting a Salesforce sandbox org.
21
+ flowsprite mcp install --client claude-code
22
+ Point Claude Code at this workspace.
23
+
24
+ Commands, each with its own options:
25
+ setup Run the whole setup (the default command). Steps
26
+ that are already done are skipped.
27
+ --sandbox Connect a Salesforce sandbox org (sign-in at
28
+ test.salesforce.com). Without it the sign-in is at
29
+ login.salesforce.com, the production login host.
30
+ --my-domain <host> Sign in to Salesforce at this My Domain host instead.
31
+ --no-open Do not try to open a browser for the login step.
32
+ login [--workspace <name>] [--no-gh]
33
+ Log this terminal in. Uses the GitHub CLI's login
34
+ (gh) when it has one, else a device code in the
35
+ browser.
36
+ --workspace <name> The name for the workspace the GitHub CLI login
37
+ creates. Omitted, login asks at a terminal; through
38
+ a pipe it uses the browser instead.
39
+ --no-gh Skip the GitHub CLI and use the browser.
40
+ --no-open Do not try to open a browser.
41
+ --token <token> Use this token instead of logging in.
42
+ --save With --token: also store it for this endpoint.
9
43
  logout Forget the stored credential for this endpoint.
10
44
  status Show the setup checklist, connections and installations.
11
45
  connect salesforce Get a one-time link to connect a Salesforce org.
46
+ Takes --sandbox and --my-domain, as setup does.
47
+ connect salesforce --from-sf-cli <alias>
48
+ Connect an org the Salesforce CLI is already logged
49
+ in to (no browser). Needs sf on PATH and a web
50
+ login (not JWT).
12
51
  connect github Get a one-time link to install the GitHub app.
13
- bind Bind a GitHub repo to a Salesforce connection. With no
14
- --repo/--create it offers the repos setup_status lists.
52
+ bind Give a Salesforce connection a GitHub repo. With no
53
+ --repo/--create it offers to create a new private
54
+ one with a suggested name, or pick an existing one.
55
+ --connection <id> The Salesforce connection id (also for sync).
56
+ --installation <number> The GitHub installation id.
57
+ --repo <owner/name> Use a repository that already exists.
58
+ --create <name> Create a new repository with this name, without
59
+ asking. Always PRIVATE — Flowsprite only mirrors
60
+ into private repos.
15
61
  sync Start a metadata sync and show the recent sync log.
62
+ Takes --connection, as bind does.
16
63
  doctor Check node, the endpoint, the credential, and sf/gh.
64
+ Exits 1 when a check FAILs.
17
65
  mcp install Point an AI client at this workspace's MCP endpoint.
66
+ --client <name> claude-code, codex, claude-desktop or cursor.
67
+ Omitted, mcp install offers the ones it detects and
68
+ mcp print shows every one.
69
+ --print Show what it would do, change nothing.
70
+ --force Replace an existing flowsprite entry.
71
+ --token-env <NAME> codex reads the bearer from this variable;
72
+ claude-code uses its CURRENT value.
18
73
  mcp print Show the config snippet for a client, token hidden.
74
+ Takes --client, as mcp install does.
75
+ --reveal Print the real token instead of <YOUR_TOKEN>. Only
76
+ at a terminal, never through a pipe.
19
77
 
20
- Options:
78
+ Options for every command:
21
79
  --endpoint <origin> Flowsprite origin (default https://app.flowsprite.com).
22
80
  --json Machine-readable output where a command has one.
23
81
  --token <token> Use this token instead of the stored credential.
24
- --save With --token: also store it for this endpoint.
25
- --no-open Do not try to open a browser during login.
26
- --sandbox connect salesforce: use the sandbox login host.
27
- --my-domain <host> connect salesforce: use this My Domain login host.
28
- --connection <id> bind / sync: the Salesforce connection id.
29
- --installation <number> bind: the GitHub installation id.
30
- --repo <owner/name> bind: a repository that already exists.
31
- --create <name> bind: create a new repository instead. Always PRIVATE —
32
- Flowsprite only mirrors into private repos.
33
- --client <name> mcp: claude-code, codex, claude-desktop or cursor.
34
- Omitted, mcp install offers the ones it detects and
35
- mcp print shows every one.
36
- --print mcp install: show what it would do, change nothing.
37
- --force mcp install: replace an existing flowsprite entry.
38
- --reveal mcp print: print the real token instead of
39
- <YOUR_TOKEN>. Only at a terminal, never through a pipe.
40
- --token-env <NAME> mcp install: codex reads the bearer from this
41
- variable; claude-code uses its CURRENT value.
42
82
  -h, --help Show this help and exit 0.
43
83
  -v, --version Show the version and exit 0.
44
84
 
package/dist/version.js CHANGED
@@ -7,4 +7,4 @@
7
7
  * manifest are pinned EQUAL by cli/src/packaging.test.ts, which reads both, so
8
8
  * they cannot disagree without a red test.
9
9
  */
10
- export const VERSION = "0.1.0";
10
+ export const VERSION = "0.1.4";
package/dist/wiring.js ADDED
@@ -0,0 +1,96 @@
1
+ import { createStatusLine, createWriter } from "./output.js";
2
+ import { printable, redact } from "./redact.js";
3
+ /**
4
+ * THE WRITER, THE STATUS LINE AND THE REVEAL SINK ARE BUILT HERE; the readline
5
+ * prompt (`ask`, built in bin.ts) only passes through, redacted and scrubbed, and the bin's
6
+ * last-line-of-defence catch is its own exit (see the exit list in output.ts).
7
+ * Two conditions govern what exists:
8
+ *
9
+ * - the writer (`log`/`error`, both redacting) — unconditional;
10
+ * - the status line and the ONE unredacted sink, `reveal` — only when
11
+ * `stdout.isTTY === true`. Strictly `=== true`: a real pipe carries
12
+ * `isTTY === undefined`, so `!== false` would reveal into a pipe. The sink
13
+ * writes through the SAME `stdout` object whose `isTTY` authorised it, never
14
+ * through `log` (a separate function a caller could point anywhere). Its only
15
+ * reader is `mcpPrint` in `cli.ts`.
16
+ *
17
+ * The prompt needs BOTH stdout and stdin to be terminals (`interactive`); the
18
+ * secret needs only stdout: stdin may be a here-doc while the person is still
19
+ * watching the terminal the secret lands in. The browser opener needs NEITHER —
20
+ * it is decided by `shouldOpenBrowser` (card eecc9d6f C1).
21
+ */
22
+ export function wireDeps(io) {
23
+ const stdoutIsTTY = io.stdout.isTTY === true;
24
+ const interactive = stdoutIsTTY && io.stdin.isTTY === true;
25
+ // FLO-509: the in-place login countdown exists only when stdout is a terminal
26
+ // (the login lines go to stdout). On a pipe a redrawn line is just noise in a log.
27
+ const statusLine = stdoutIsTTY ? createStatusLine((text) => io.stdout.write(text)) : null;
28
+ const writer = createWriter({
29
+ out: (line) => {
30
+ statusLine?.settle();
31
+ io.log(line);
32
+ },
33
+ err: (line) => {
34
+ statusLine?.settle();
35
+ io.error(line);
36
+ },
37
+ });
38
+ const deps = {
39
+ env: io.env,
40
+ writer,
41
+ fetch: io.fetch,
42
+ sleep: io.sleep,
43
+ now: io.now,
44
+ nodeVersion: io.nodeVersion,
45
+ isOnPath: io.isOnPath,
46
+ version: io.version,
47
+ spawn: io.spawn,
48
+ fileExists: io.fileExists,
49
+ home: io.home,
50
+ platform: io.platform,
51
+ ...(statusLine === null ? {} : { progress: statusLine.progress }),
52
+ // `mcp print --reveal` is the ONE command allowed to put a token on stdout,
53
+ // and this is the fact it is allowed on. The sink settles a dangling
54
+ // countdown first so the secret never lands glued to a status line.
55
+ ...(stdoutIsTTY
56
+ ? {
57
+ reveal: (line) => {
58
+ statusLine?.settle();
59
+ io.stdout.write(line + "\n");
60
+ },
61
+ }
62
+ : {}),
63
+ // The browser opener is NOT gated on a terminal any more (card eecc9d6f C1).
64
+ // History: FLO-500 gated it on stdin+stdout TTYs after a piped probe popped
65
+ // a tab; that also starved every agent shell (Claude Code runs commands with
66
+ // no TTY on a person's own desktop), which is exactly where `login` is now
67
+ // run most. The rule is `shouldOpenBrowser`: `--no-open` (cli.ts), a CI
68
+ // environment, or a Linux box with no display keeps the browser shut.
69
+ ...(shouldOpenBrowser(io.env, io.platform) ? { openUrl: io.openUrl } : {}),
70
+ // The prompt stays gated on there being a terminal on BOTH ends.
71
+ // The question is redacted AND scrubbed HERE because readline writes it to
72
+ // stdout raw, around the writer — the prompt text is CLI-built (a path, a
73
+ // display name), but the guarantee should not depend on every future caller
74
+ // remembering. `printable`, not the writer's per-line scrub: a question is
75
+ // ONE line by construction, so a newline in a spliced display name is a
76
+ // forged line, not structure.
77
+ ...(interactive ? { ask: (question) => io.ask(printable(redact(question))) } : {}),
78
+ };
79
+ return { deps, statusLine };
80
+ }
81
+ /**
82
+ * C1 — may `login` open a browser on this machine? Yes, unless a CI job (`CI`
83
+ * set to anything non-empty — the convention GitHub Actions, GitLab and most
84
+ * runners follow) or Linux with neither `DISPLAY` nor `WAYLAND_DISPLAY` (an SSH
85
+ * session or a container: `xdg-open` there has no screen, or picks a text
86
+ * browser that takes over the terminal). `--no-open` is the person's override,
87
+ * applied in `cli.ts`. PURE, so the whole cross product is pinned in wiring.test.ts.
88
+ */
89
+ export function shouldOpenBrowser(env, platform) {
90
+ const set = (name) => typeof env[name] === "string" && env[name].length > 0;
91
+ if (set("CI"))
92
+ return false;
93
+ if (platform === "linux" && !set("DISPLAY") && !set("WAYLAND_DISPLAY"))
94
+ return false;
95
+ return true;
96
+ }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "flowsprite",
3
- "version": "0.1.0",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "description": "The Flowsprite CLI: log a terminal in to Flowsprite and drive workspace setup over the MCP endpoint. Zero runtime dependencies, node builtins only.",
6
6
  "license": "UNLICENSED",
7
7
  "homepage": "https://app.flowsprite.com/setup",
8
8
  "bin": {
9
- "flowsprite": "./dist/bin.js"
9
+ "flowsprite": "dist/bin.js"
10
10
  },
11
11
  "engines": {
12
12
  "node": ">=20"