flostep 0.1.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/AGENTS.md ADDED
@@ -0,0 +1,105 @@
1
+ # Using Flostep from an agent
2
+
3
+ Run `npx flostep init` at the root of your repository to write the block below into your agent's instruction files (`AGENTS.md`, `CLAUDE.md`, `.cursor/rules`). Or paste it into your agent's context yourself — a system prompt, for instance — to let it build and share diagrams with a Bash tool alone.
4
+
5
+ ---
6
+
7
+ ## Flostep CLI
8
+
9
+ Flostep makes diagrams people can **step through** one interaction at a time, share by link, or embed. Use it when the user wants a walkable, shareable diagram of how something works — a request crossing services, an approval chain, a customer journey. Not for class, ER, state or Gantt diagrams, and it does not emit markup to paste into a file.
10
+
11
+ Run `npx flostep` (Node 20+). Authenticated via `FLOSTEP_TOKEN`, or `flostep login` for a browser flow.
12
+
13
+ ### Format
14
+
15
+ One step per line, in the order it happens:
16
+
17
+ ```
18
+ Frontend -> API: POST /login
19
+ API -> Database: check credentials
20
+ Database -> API: user record
21
+ API -> Frontend: returns JWT
22
+ ```
23
+
24
+ Components are created the first time they are named — reuse a name to reference the same box, and give genuinely different components different names. The `: description` is optional. Point a component at itself for internal work. Participants can be people, teams, or systems, not only software.
25
+
26
+ Limits: 40 components, 120 steps. Run `flostep syntax` for the authoritative grammar.
27
+
28
+ ### Building a diagram
29
+
30
+ Write the whole flow at once — atomic, and one call instead of eight. **Pipe it in; do not create files** in the user's repository unless they asked for a diagram that lives there:
31
+
32
+ ```bash
33
+ npx flostep create --title "Checkout" --share <<'EOF'
34
+ Customer -> API: POST /checkout
35
+ API -> Payments: charge card
36
+ API -> Customer: order confirmed
37
+ EOF
38
+ # ✓ Created #42 Checkout
39
+ # https://flostep.dev/s/rEFdW8GSDwQ <- give the user this
40
+ ```
41
+
42
+ `--share` returns the public link in the same call. Add `--json` for a parseable result.
43
+
44
+ For a diagram that already exists, share it on its own:
45
+
46
+ ```bash
47
+ npx flostep share 42 # prints the public link
48
+ npx flostep share 42 --embed # iframe URL, for a docs page
49
+ ```
50
+
51
+ If the user keeps the steps in a file in their repo, pipe the file in and let them keep the file — the CLI tracks nothing on disk:
52
+
53
+ ```bash
54
+ npx flostep update 42 < docs/checkout.flostep
55
+ ```
56
+
57
+ To rewrite an existing diagram, read it, transform it, pipe it back:
58
+
59
+ ```bash
60
+ npx flostep show 42 | sed 's/Redis/Session Cache/' | npx flostep update 42
61
+ ```
62
+
63
+ Or change one step at a time when you have no local file:
64
+
65
+ ```bash
66
+ npx flostep show 42 # read it first
67
+ npx flostep step add 42 "API -> Cache: read session"
68
+ npx flostep step add 42 "Client -> API: retry" --at 3
69
+ npx flostep step rm 42 5
70
+ npx flostep node rename 42 "Redis" "Session Cache"
71
+ ```
72
+
73
+ ### Folders
74
+
75
+ Folders are shared by a team and addressed by name. File a diagram only when the user asks you to organise it:
76
+
77
+ ```bash
78
+ npx flostep folder list --json # names and diagram counts
79
+ npx flostep folder create "Payments" # returns the folder if it already exists
80
+ npx flostep move 42 "Payments" # file it; --none takes it out
81
+ npx flostep list --folder "Payments" --json
82
+ ```
83
+
84
+ `flostep folder rename` and `flostep folder delete` exist too — ask before either; a delete un-files every diagram in it for the whole team, and needs `--yes` without a terminal.
85
+
86
+ ### Reading
87
+
88
+ ```bash
89
+ npx flostep list --json # ids, titles, urls
90
+ npx flostep show 42 # the steps, plain text
91
+ npx flostep step list 42 --json # numbered steps
92
+ npx flostep node list 42 --json # components, first-appearance order
93
+ npx flostep whoami --json # which workspace you're writing to
94
+ ```
95
+
96
+ ### Rules
97
+
98
+ - **Read before you write.** `create`, `update`, `step` and `node` all replace the whole diagram; `show` first so you don't discard steps.
99
+ - **Changes made since you read are protected.** `step` and `node` are refused if the diagram changed after they read it; for `update`, pass the `version` from `show --json` as `--if-version`. On a refusal, `show` again and redo your change on the current steps — never retry the same write.
100
+ - **Don't leave files behind.** No command writes to disk. Redirect `show` yourself if the user asks for the steps in a file.
101
+ - **Never invent a diagram id.** Get it from `list`, `create`, or the user.
102
+ - **Exit codes**: `0` success, `1` error, `2` usage. Errors go to stderr with the reason.
103
+ - **There is no `node add` and no `--type`.** The format cannot express an unconnected component or an explicit type — types are inferred from the name. Add a component by naming it in a step.
104
+ - **Notes, positions and hand-drawn curves are not in the text format** and are dropped by any write. Say so if the user has them.
105
+ - Ask before `delete`. It needs `--yes` when there's no terminal, and it cannot be undone.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Flostep
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # flostep
2
+
3
+ Build, update and share [Flostep](https://flostep.dev) diagrams from the terminal, from CI, or from a coding agent.
4
+
5
+ Flostep turns ordered interactions into a diagram people can **step through** — one hop at a time, shared by link or embedded in a doc. This CLI builds one from a pipe and hands back the link, writing nothing to disk.
6
+
7
+ ```bash
8
+ npx flostep create --title "Checkout" --share < flow.txt
9
+ ```
10
+
11
+ No install needed. Requires Node 20 or newer.
12
+
13
+ ## Quick start
14
+
15
+ ```bash
16
+ npx flostep login # approve in a browser
17
+
18
+ npx flostep create --title "Checkout" --share <<'EOF'
19
+ Customer -> API: POST /checkout
20
+ API -> Payments: charge card
21
+ API -> Customer: order confirmed
22
+ EOF
23
+ # ✓ Created #42 Checkout
24
+ # https://flostep.dev/s/rEFdW8GSDwQ
25
+ ```
26
+
27
+ That wrote nothing to disk, and the id it printed is what every other command takes:
28
+
29
+ ```bash
30
+ npx flostep show 42 | sed 's/Payments/Stripe/' | npx flostep update 42
31
+ ```
32
+
33
+ ## The format
34
+
35
+ One step per line, in the order it happens:
36
+
37
+ ```
38
+ Customer -> API: POST /checkout
39
+ API -> Payments: charge card
40
+ Payments -> API: authorized
41
+ API -> Customer: order confirmed
42
+ ```
43
+
44
+ Components are created the first time they are named, so reusing a name references the same box. The `: description` is optional. Point a component at itself for internal work.
45
+
46
+ Participants don't have to be software — a refund escalating from a customer to support to finance models exactly as well as a request crossing three services.
47
+
48
+ Run `flostep syntax` for the authoritative version, fetched from the server.
49
+
50
+ ## Commands
51
+
52
+ | Command | |
53
+ | --- | --- |
54
+ | `flostep login` | sign in from a browser (`--token` to paste a key instead) |
55
+ | `flostep logout` | forget the stored credentials |
56
+ | `flostep whoami` | which account and workspace a key writes to |
57
+ | `flostep init` | add the agent instructions to AGENTS.md / CLAUDE.md / `.cursor/rules` |
58
+ | `flostep list` | the diagrams in the workspace (`--folder` to narrow) |
59
+ | `flostep show <id>` | print a diagram as steps (pipeable) |
60
+ | `flostep create` | create from stdin, writing no files (`--share` for a link) |
61
+ | `flostep update <id>` | replace a diagram's steps from stdin (`--if-version` to refuse if it changed) |
62
+ | `flostep share <id>` | public link on/off (`--embed` for an iframe) |
63
+ | `flostep open <id>` | open the editor in a browser |
64
+ | `flostep delete <id>` | delete a diagram |
65
+ | `flostep folder list\|create\|rename\|delete` | manage the workspace's folders |
66
+ | `flostep move <id> <folder>` | file a diagram in a folder (`--none` to take it out) |
67
+ | `flostep step add\|rm\|list` | edit individual steps |
68
+ | `flostep node rename\|list` | rename or list components |
69
+ | `flostep syntax` | print the step grammar |
70
+
71
+ Every command takes `--json`. Exit codes are **0** success, **1** error, **2** usage.
72
+
73
+ `flostep --help --json` prints the whole command and flag surface as JSON, derived from the command definitions themselves — useful for tooling, and what the hosted docs are tested against.
74
+
75
+ Ids come from `flostep list` or from `create`. Nothing is resolved from a file path — diagrams live in your account, not in a checkout.
76
+
77
+ ## In CI
78
+
79
+ There is no browser in CI, so the device flow doesn't apply. Create a key at [flostep.dev/api_keys](https://flostep.dev/api_keys) — leave its expiry as **Never**, since a key that lapses takes the pipeline down on a date nobody chose — and set it as `FLOSTEP_TOKEN`.
80
+
81
+ ```yaml
82
+ - run: npx flostep update 42 < docs/checkout.flostep
83
+ env:
84
+ FLOSTEP_TOKEN: ${{ secrets.FLOSTEP_TOKEN }}
85
+ ```
86
+
87
+ Keeping the steps in a file and piping them in is up to you — the CLI tracks nothing on disk. Exit codes make the step fail loudly: **1** on an error, **2** on a usage mistake.
88
+
89
+ ## Environment
90
+
91
+ | | |
92
+ | --- | --- |
93
+ | `FLOSTEP_TOKEN` | API key. Takes precedence over a stored login. |
94
+ | `NO_COLOR` | Disable colour. Colour is off automatically when not a TTY. |
95
+
96
+ Credentials are stored at `~/.config/flostep/config.json`, mode `0600`.
97
+
98
+ ## For coding agents
99
+
100
+ The CLI is designed to be driven by an agent with a Bash tool — no MCP client or per-tool config required. `--json` on every read command, errors on stderr, deterministic exit codes, and `flostep --help` is self-contained.
101
+
102
+ To make an agent working in your repository find it on its own, run this at the repo root:
103
+
104
+ ```bash
105
+ npx flostep init
106
+ ```
107
+
108
+ It writes the block from [AGENTS.md](AGENTS.md) into every agent instruction file the repo already has — `AGENTS.md`, `CLAUDE.md`, `.cursor/rules/flostep.mdc` — or creates `AGENTS.md` if there are none. The block sits between marker comments, so re-running it after an upgrade updates it in place and leaves the rest of the file alone. It lists the files and asks before writing when run in a terminal (`--yes` skips that); in CI or with `--json` it just writes. `--file <path>` picks the file yourself; `--print` writes it to stdout for a system prompt.
109
+
110
+ ## License
111
+
112
+ MIT
package/bin/flostep.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ // Thin shim: everything real is in src/, so this file never needs changing and
3
+ // the exit code is set in exactly one place.
4
+ import { run } from "../src/cli.js";
5
+
6
+ process.exitCode = await run(process.argv.slice(2));
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "flostep",
3
+ "version": "0.1.0",
4
+ "description": "Build, update and share Flostep diagrams from the terminal, from CI, or from a coding agent.",
5
+ "type": "module",
6
+ "bin": {
7
+ "flostep": "bin/flostep.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "README.md",
13
+ "AGENTS.md",
14
+ "LICENSE"
15
+ ],
16
+ "engines": {
17
+ "node": ">=20"
18
+ },
19
+ "scripts": {
20
+ "test": "node --test"
21
+ },
22
+ "keywords": [
23
+ "flostep",
24
+ "diagram",
25
+ "sequence-diagram",
26
+ "architecture",
27
+ "diagrams-as-code",
28
+ "cli"
29
+ ],
30
+ "license": "MIT",
31
+ "homepage": "https://flostep.dev/docs",
32
+ "bugs": {
33
+ "email": "support@flostep.dev"
34
+ },
35
+ "author": "Flostep <support@flostep.dev>"
36
+ }
package/src/api.js ADDED
@@ -0,0 +1,197 @@
1
+ // The HTTP client. Zero dependencies — Node's own fetch is enough.
2
+ //
3
+ // The server's error strings are already written for people ("Free plan limit
4
+ // of 3 diagrams reached. Delete one, or upgrade…"), so this deliberately passes
5
+ // them through rather than re-wording them. Restating them here would mean two
6
+ // copies of the same sentence drifting apart across a repo boundary, which is
7
+ // the exact problem `flostep syntax` exists to avoid.
8
+
9
+ import { CliError } from "./errors.js";
10
+ import { USER_AGENT } from "./version.js";
11
+
12
+ const TIMEOUT_MS = 30_000;
13
+
14
+ // A ceiling on how many pages `listDiagrams` will follow — 5,000 diagrams at the
15
+ // server's 100 a page. Far past any real library; it exists so a server that
16
+ // misreports its total can't keep the loop going forever.
17
+ const MAX_PAGES = 50;
18
+
19
+ export class Api {
20
+ constructor({ host, token }) {
21
+ this.host = host;
22
+ this.token = token;
23
+ }
24
+
25
+ async request(method, path, body) {
26
+ return (await this.#send(method, path, body)).payload;
27
+ }
28
+
29
+ async #send(method, path, body) {
30
+ const headers = { Accept: "application/json", "User-Agent": USER_AGENT };
31
+ if (this.token) headers.Authorization = `Bearer ${this.token}`;
32
+ if (body !== undefined) headers["Content-Type"] = "application/json";
33
+
34
+ let response;
35
+ try {
36
+ response = await fetch(`${this.host}${path}`, {
37
+ method,
38
+ headers,
39
+ body: body === undefined ? undefined : JSON.stringify(body),
40
+ signal: AbortSignal.timeout(TIMEOUT_MS)
41
+ });
42
+ } catch (cause) {
43
+ // A refused connection and a wrong host look identical from here, and the
44
+ // host is the thing worth naming — it is usually a stale FLOSTEP_API_URL.
45
+ throw new CliError(`Could not reach ${this.host}.`, {
46
+ hint: cause?.name === "TimeoutError"
47
+ ? "The request timed out after 30s."
48
+ : "Check your connection."
49
+ });
50
+ }
51
+
52
+ // 204 has no body, and neither does anything that failed to serialise.
53
+ const text = await response.text();
54
+ let payload = null;
55
+ if (text) {
56
+ try {
57
+ payload = JSON.parse(text);
58
+ } catch {
59
+ payload = null;
60
+ }
61
+ }
62
+
63
+ if (!response.ok) throw this.#toError(response, payload, path);
64
+ return { payload, response };
65
+ }
66
+
67
+ #toError(response, payload, path) {
68
+ const message = payload?.error;
69
+
70
+ if (response.status === 401) {
71
+ // The server distinguishes expired from invalid, and that distinction is
72
+ // the whole reason it does: "log in again" and "you copied it wrong" are
73
+ // different fixes. Passing its sentence through keeps them distinct.
74
+ return new CliError(message || "Not authenticated.", {
75
+ hint: this.token ? "Run `flostep login` to sign in again." : "Run `flostep login` first."
76
+ });
77
+ }
78
+
79
+ if (response.status >= 500) {
80
+ return new CliError(`Flostep returned ${response.status}.`, {
81
+ hint: "This is a problem on the server's side — try again shortly."
82
+ });
83
+ }
84
+
85
+ // The server's sentence says what happened; this says what to do. Not
86
+ // retried: the change was computed from a copy that is now out of date, so
87
+ // replaying it would be the very overwrite the check exists to stop.
88
+ if (response.status === 409) {
89
+ return new CliError(message || "The diagram changed since it was read.", {
90
+ hint: "Run `flostep show <id>` to see the current steps, then make your change again."
91
+ });
92
+ }
93
+
94
+ // The server says "Not Found", which is true and useless: it names neither
95
+ // what was missing nor where it looked. An id from another workspace — or
96
+ // from a local server, when the CLI is pointed at the public one — is the
97
+ // usual cause, and the host is the part that makes that obvious.
98
+ if (response.status === 404) {
99
+ const diagram = path.match(/\/diagrams\/(\d+)/)?.[1];
100
+ const folder = /\/folders\//.test(path);
101
+ const what = diagram ? `Diagram #${diagram}` : folder ? "That folder" : "That";
102
+ return new CliError(`${what} isn't on ${this.host}.`, {
103
+ hint: diagram
104
+ ? "Run `flostep list` to see the ids in this workspace."
105
+ : "Run `flostep folder list` to see what's there."
106
+ });
107
+ }
108
+
109
+ // 402 (subscription lapsed), 403 (plan limit) and 422 (bad steps) all
110
+ // arrive with a sentence written for the person reading it.
111
+ return new CliError(message || `Request failed (${response.status}).`);
112
+ }
113
+
114
+ get(path) { return this.request("GET", path); }
115
+ post(path, body) { return this.request("POST", path, body ?? {}); }
116
+ put(path, body) { return this.request("PUT", path, body ?? {}); }
117
+ del(path) { return this.request("DELETE", path); }
118
+
119
+ // --- diagrams ---------------------------------------------------------
120
+
121
+ // Every diagram, following the server's pages. The list used to be one
122
+ // request capped at 100 with nothing to say it had been cut, so a diagram
123
+ // past that simply didn't exist as far as an agent could tell.
124
+ async listDiagrams({ folder } = {}) {
125
+ const diagrams = [];
126
+
127
+ for (let page = 1; page <= MAX_PAGES; page++) {
128
+ const query = new URLSearchParams({ page: String(page) });
129
+ if (folder !== undefined) query.set("folder", String(folder));
130
+
131
+ const { payload, response } = await this.#send("GET", `/api/v1/diagrams?${query}`);
132
+ diagrams.push(...payload);
133
+
134
+ // A server from before paging sends no total and ignores `page` — asking
135
+ // for page 2 would return page 1 again — so no header means stop here.
136
+ const total = Number(response.headers.get("x-total-count"));
137
+ if (!response.headers.has("x-total-count") || payload.length === 0 || diagrams.length >= total) break;
138
+ }
139
+
140
+ return diagrams;
141
+ }
142
+ getDiagram(id) { return this.get(`/api/v1/diagrams/${encodeURIComponent(id)}`); }
143
+ createDiagram({ title, code }) { return this.post("/api/v1/diagrams", { title, code }); }
144
+ updateDiagram(id, body) { return this.put(`/api/v1/diagrams/${encodeURIComponent(id)}`, body); }
145
+ deleteDiagram(id) { return this.del(`/api/v1/diagrams/${encodeURIComponent(id)}`); }
146
+ shareDiagram(id) { return this.post(`/api/v1/diagrams/${encodeURIComponent(id)}/share`); }
147
+ unshareDiagram(id) { return this.del(`/api/v1/diagrams/${encodeURIComponent(id)}/share`); }
148
+ moveDiagram(id, folderId) {
149
+ return this.put(`/api/v1/diagrams/${encodeURIComponent(id)}/folder`, { folder_id: folderId });
150
+ }
151
+
152
+ // --- folders ----------------------------------------------------------
153
+
154
+ listFolders() { return this.get("/api/v1/folders"); }
155
+ createFolder(name) { return this.post("/api/v1/folders", { name }); }
156
+ renameFolder(id, name) { return this.put(`/api/v1/folders/${encodeURIComponent(id)}`, { name }); }
157
+ deleteFolder(id) { return this.del(`/api/v1/folders/${encodeURIComponent(id)}`); }
158
+
159
+ // Folders are addressed by name — it's what people say, and names are unique
160
+ // per workspace ignoring case — so the id the endpoints want is looked up
161
+ // here. An unknown name is an error naming the real folders, never an
162
+ // implicit create: that turns a misspelt "Paymnets" into a near-duplicate.
163
+ async findFolder(name) {
164
+ const { folders } = await this.listFolders();
165
+ const wanted = String(name).trim().toLowerCase();
166
+ const folder = folders.find((f) => f.name.toLowerCase() === wanted);
167
+ if (folder) return folder;
168
+
169
+ throw new CliError(`No folder named "${String(name).trim()}".`, {
170
+ hint: folders.length
171
+ ? `Existing folders: ${folders.map((f) => f.name).join(", ")}. Create one with \`flostep folder create\`.`
172
+ : "This workspace has no folders yet. Create one with `flostep folder create <name>`."
173
+ });
174
+ }
175
+
176
+ me() { return this.get("/api/v1/me"); }
177
+ syntax() { return this.get("/api/v1/syntax"); }
178
+
179
+ // --- device flow ------------------------------------------------------
180
+
181
+ requestDeviceCode(clientName) {
182
+ return this.post("/api/v1/device/code", { client_name: clientName });
183
+ }
184
+
185
+ // Polling is expected to fail most of the time — `authorization_pending` is
186
+ // the normal case — so this returns the payload instead of throwing, and the
187
+ // caller drives the state machine.
188
+ async pollDeviceToken(deviceCode) {
189
+ const response = await fetch(`${this.host}/api/v1/device/token`, {
190
+ method: "POST",
191
+ headers: { "Content-Type": "application/json", Accept: "application/json", "User-Agent": USER_AGENT },
192
+ body: JSON.stringify({ device_code: deviceCode }),
193
+ signal: AbortSignal.timeout(TIMEOUT_MS)
194
+ });
195
+ return response.json();
196
+ }
197
+ }
package/src/browser.js ADDED
@@ -0,0 +1,46 @@
1
+ // Opening a URL in the user's browser, best-effort.
2
+ //
3
+ // ⚠️ `spawn` does not throw when the opener is missing. It returns a child that
4
+ // emits 'error' on a later tick, and with no listener that is an uncaught
5
+ // exception — so the try/catch this used to sit in never fired, and `flostep
6
+ // login` died with ENOENT on exactly the machines the device grant exists for
7
+ // (a container, an SSH box, a minimal Linux image with no xdg-open), mid-spinner
8
+ // and with the cursor still hidden. The fix is to listen for both outcomes and
9
+ // resolve on whichever comes first: 'spawn' means the opener started, 'error'
10
+ // means it never did.
11
+ //
12
+ // It never rejects. Every caller has already printed the URL or is about to,
13
+ // so failing to open a browser is a fallback, not an error.
14
+
15
+ import { spawn } from "node:child_process";
16
+
17
+ // No shell anywhere. Windows used to go through `start` with `shell: true`,
18
+ // where cmd.exe reads `&` in a URL as a command separator and treats the first
19
+ // quoted argument as a window title. rundll32's URL handler takes the URL as a
20
+ // plain argument.
21
+ function opener(url) {
22
+ if (process.platform === "darwin") return ["open", [url]];
23
+ if (process.platform === "win32") return ["rundll32", ["url.dll,FileProtocolHandler", url]];
24
+ return ["xdg-open", [url]];
25
+ }
26
+
27
+ export function openBrowser(url) {
28
+ const [command, args] = opener(url);
29
+
30
+ return new Promise((resolve) => {
31
+ let child;
32
+ try {
33
+ child = spawn(command, args, { stdio: "ignore", detached: true });
34
+ } catch {
35
+ // Synchronous failures exist too (a malformed argument, EACCES on some
36
+ // platforms), so this stays alongside the listener rather than instead of it.
37
+ resolve(false);
38
+ return;
39
+ }
40
+ child.once("error", () => resolve(false));
41
+ child.once("spawn", () => {
42
+ child.unref();
43
+ resolve(true);
44
+ });
45
+ });
46
+ }