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 +105 -0
- package/LICENSE +21 -0
- package/README.md +112 -0
- package/bin/flostep.js +6 -0
- package/package.json +36 -0
- package/src/api.js +197 -0
- package/src/browser.js +46 -0
- package/src/cli.js +264 -0
- package/src/code.js +151 -0
- package/src/commands/create.js +55 -0
- package/src/commands/delete.js +59 -0
- package/src/commands/folder.js +141 -0
- package/src/commands/init.js +106 -0
- package/src/commands/list.js +56 -0
- package/src/commands/login.js +166 -0
- package/src/commands/logout.js +30 -0
- package/src/commands/move.js +48 -0
- package/src/commands/node.js +87 -0
- package/src/commands/open.js +29 -0
- package/src/commands/share.js +47 -0
- package/src/commands/show.js +30 -0
- package/src/commands/step.js +129 -0
- package/src/commands/syntax.js +17 -0
- package/src/commands/update.js +71 -0
- package/src/commands/whoami.js +39 -0
- package/src/config.js +67 -0
- package/src/errors.js +26 -0
- package/src/instructions.js +121 -0
- package/src/output.js +170 -0
- package/src/source.js +63 -0
- package/src/stdin.js +29 -0
- package/src/target.js +30 -0
- package/src/version.js +17 -0
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
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
|
+
}
|