flostep 0.1.4 → 0.1.5
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 +3 -3
- package/package.json +1 -1
- package/src/api.js +10 -2
- package/src/commands/list.js +6 -3
- package/src/commands/login.js +51 -7
- package/src/commands/whoami.js +4 -1
package/README.md
CHANGED
|
@@ -51,9 +51,9 @@ Run `flostep syntax` for the authoritative version, fetched from the server.
|
|
|
51
51
|
|
|
52
52
|
| Command | |
|
|
53
53
|
| --- | --- |
|
|
54
|
-
| `flostep login` | sign in from a browser (`--token` to paste a key instead) |
|
|
54
|
+
| `flostep login` | sign in from a browser (`--read-only` for a key that can't change anything, `--token` to paste a key instead) |
|
|
55
55
|
| `flostep logout` | forget the stored credentials |
|
|
56
|
-
| `flostep whoami` | which account and workspace a key writes to |
|
|
56
|
+
| `flostep whoami` | which account and workspace a key writes to, and whether it can write |
|
|
57
57
|
| `flostep init` | add the agent instructions to AGENTS.md / CLAUDE.md / `.cursor/rules` |
|
|
58
58
|
| `flostep list` | the diagrams in the workspace (`--folder` to narrow) |
|
|
59
59
|
| `flostep show <id>` | print a diagram as steps (pipeable) |
|
|
@@ -90,7 +90,7 @@ Keeping the steps in a file and piping them in is up to you — the CLI tracks n
|
|
|
90
90
|
|
|
91
91
|
| | |
|
|
92
92
|
| --- | --- |
|
|
93
|
-
| `FLOSTEP_TOKEN` | API key. Takes precedence over a stored login. |
|
|
93
|
+
| `FLOSTEP_TOKEN` | API key. Takes precedence over a stored login. For a job that only reads, create a **Read only** key on the API keys page. |
|
|
94
94
|
| `NO_COLOR` | Disable colour. Colour is off automatically when not a TTY. |
|
|
95
95
|
|
|
96
96
|
Credentials are stored at `~/.config/flostep/config.json`, mode `0600`.
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -21,6 +21,7 @@ export class Api {
|
|
|
21
21
|
this.host = host;
|
|
22
22
|
this.token = token;
|
|
23
23
|
this.tokenSource = tokenSource;
|
|
24
|
+
this.keyAccess = null;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
async request(method, path, body) {
|
|
@@ -50,6 +51,11 @@ export class Api {
|
|
|
50
51
|
});
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
// What the server says this key may do ("read" or "write"), from every
|
|
55
|
+
// authenticated response. Null against a server from before access levels,
|
|
56
|
+
// where every key could write — callers treat null as "don't know".
|
|
57
|
+
if (response.headers.has("x-key-access")) this.keyAccess = response.headers.get("x-key-access");
|
|
58
|
+
|
|
53
59
|
// 204 has no body, and neither does anything that failed to serialise.
|
|
54
60
|
const text = await response.text();
|
|
55
61
|
let payload = null;
|
|
@@ -186,8 +192,10 @@ export class Api {
|
|
|
186
192
|
|
|
187
193
|
// --- device flow ------------------------------------------------------
|
|
188
194
|
|
|
189
|
-
|
|
190
|
-
|
|
195
|
+
// `access: "read"` asks for a read-only key. Omitted otherwise, rather than
|
|
196
|
+
// sent as "write", so the request is byte-for-byte what older CLIs sent.
|
|
197
|
+
requestDeviceCode(clientName, { access } = {}) {
|
|
198
|
+
return this.post("/api/v1/device/code", { client_name: clientName, ...(access ? { access } : {}) });
|
|
191
199
|
}
|
|
192
200
|
|
|
193
201
|
// Polling is expected to fail most of the time — `authorization_pending` is
|
package/src/commands/list.js
CHANGED
|
@@ -38,9 +38,12 @@ export default {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
if (diagrams.length === 0) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
// The create hint is the obvious next step, except for a read-only key,
|
|
42
|
+
// which the server would refuse — so that key is told why instead.
|
|
43
|
+
const emptyLibrary = api.keyAccess === "read"
|
|
44
|
+
? "No diagrams yet. This key is read-only, so it can't create one — use a read & write key."
|
|
45
|
+
: "No diagrams yet. Create one with `echo 'A -> B: hello' | flostep create --title X --share`.";
|
|
46
|
+
out(dim(folder === undefined ? emptyLibrary : "No diagrams in that folder."));
|
|
44
47
|
return 0;
|
|
45
48
|
}
|
|
46
49
|
|
package/src/commands/login.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { hostname } from "node:os";
|
|
13
13
|
|
|
14
|
-
import { CliError } from "../errors.js";
|
|
14
|
+
import { CliError, UsageError } from "../errors.js";
|
|
15
15
|
import { readConfig, writeConfig, resolveHost } from "../config.js";
|
|
16
16
|
import { note, json, box, banner, fields, startSpinner, dim, bold, cyan, green } from "../output.js";
|
|
17
17
|
import { Api } from "../api.js";
|
|
@@ -23,6 +23,7 @@ export default {
|
|
|
23
23
|
name: "login",
|
|
24
24
|
usage: [
|
|
25
25
|
"flostep login",
|
|
26
|
+
"flostep login --read-only",
|
|
26
27
|
"flostep login --token <fls_...>"
|
|
27
28
|
],
|
|
28
29
|
details: [
|
|
@@ -31,15 +32,18 @@ export default {
|
|
|
31
32
|
],
|
|
32
33
|
options: {
|
|
33
34
|
token: { type: "string" },
|
|
34
|
-
"no-browser": { type: "boolean", default: false }
|
|
35
|
+
"no-browser": { type: "boolean", default: false },
|
|
36
|
+
"read-only": { type: "boolean", default: false }
|
|
35
37
|
},
|
|
36
38
|
optionHelp: [
|
|
39
|
+
["--read-only", "ask for a key that can read diagrams but not change, delete or share them"],
|
|
37
40
|
["--token <fls_...>", "store a key you already have instead of opening a browser"],
|
|
38
41
|
["--no-browser", "print the URL rather than opening it"]
|
|
39
42
|
],
|
|
40
43
|
examples: [
|
|
41
44
|
["flostep login", "approve in a browser on any device"],
|
|
42
45
|
["flostep login --no-browser", "for SSH sessions where nothing can open"],
|
|
46
|
+
["flostep login --read-only", "for a machine that should only look"],
|
|
43
47
|
["FLOSTEP_TOKEN=fls_… flostep list", "CI needs no login at all — set the env var"]
|
|
44
48
|
],
|
|
45
49
|
|
|
@@ -48,10 +52,20 @@ export default {
|
|
|
48
52
|
|
|
49
53
|
if (!ctx.json) banner();
|
|
50
54
|
|
|
55
|
+
// A pasted key already has whatever access it was created with; there is
|
|
56
|
+
// nothing this flag could change about it, and silently ignoring it would
|
|
57
|
+
// leave someone believing a write key was read-only.
|
|
58
|
+
if (values.token && values["read-only"]) {
|
|
59
|
+
throw new UsageError("--read-only applies to the browser login, not to a pasted key.", {
|
|
60
|
+
hint: "A key keeps the access it was created with — create a read-only one on the API keys page."
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
51
64
|
if (values.token) return storeToken({ host, token: values.token.trim(), ctx });
|
|
52
65
|
|
|
66
|
+
const readOnly = values["read-only"];
|
|
53
67
|
const api = ctx.api();
|
|
54
|
-
const grant = await api.requestDeviceCode(hostname());
|
|
68
|
+
const grant = await api.requestDeviceCode(hostname(), readOnly ? { access: "read" } : {});
|
|
55
69
|
|
|
56
70
|
note();
|
|
57
71
|
box([ `Your code: ${bold(cyan(grant.user_code))}` ]);
|
|
@@ -74,6 +88,16 @@ export default {
|
|
|
74
88
|
stopSpinner();
|
|
75
89
|
}
|
|
76
90
|
|
|
91
|
+
// A server from before access levels ignores the request and mints a write
|
|
92
|
+
// key. Storing it would leave a credential that can change everything on a
|
|
93
|
+
// machine its owner asked to keep read-only, so it is refused instead — the
|
|
94
|
+
// key exists, though, and only the website can revoke it.
|
|
95
|
+
if (readOnly && token.access !== "read") {
|
|
96
|
+
throw new CliError("This Flostep server doesn't support read-only keys, so the key it created can write.", {
|
|
97
|
+
hint: `It was not saved. Revoke it on your API keys page (${host}/api-keys).`
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
77
101
|
return storeToken({ host, token: token.token, expiresAt: token.expires_at, ctx });
|
|
78
102
|
}
|
|
79
103
|
};
|
|
@@ -139,7 +163,7 @@ async function storeToken({ host, token, expiresAt, ctx }) {
|
|
|
139
163
|
const me = await new Api({ host, token }).me();
|
|
140
164
|
|
|
141
165
|
if (ctx.json) {
|
|
142
|
-
json({ email: me.email, workspace: me.workspace, host, expires_at: expiresAt ?? null });
|
|
166
|
+
json({ email: me.email, workspace: me.workspace, access: me.access ?? null, host, expires_at: expiresAt ?? null });
|
|
143
167
|
return 0;
|
|
144
168
|
}
|
|
145
169
|
|
|
@@ -149,15 +173,35 @@ async function storeToken({ host, token, expiresAt, ctx }) {
|
|
|
149
173
|
fields([
|
|
150
174
|
[ "Account", me.email ],
|
|
151
175
|
[ "Workspace", `${me.workspace.name}${me.workspace.team ? dim(" (team)") : ""}` ],
|
|
152
|
-
[ "Plan", me.workspace
|
|
176
|
+
[ "Plan", formatPlan(me.workspace) ],
|
|
177
|
+
...(me.access ? [ [ "Access", formatAccess(me.access) ] ] : []),
|
|
153
178
|
...(expiresAt ? [ [ "Expires", formatExpiry(expiresAt) ] ] : [])
|
|
154
179
|
]);
|
|
155
180
|
note();
|
|
156
|
-
|
|
157
|
-
|
|
181
|
+
// The usual next step creates a diagram, which a read-only key would refuse.
|
|
182
|
+
if (me.access === "read") {
|
|
183
|
+
note(dim(` Next: ${cyan("flostep list")}`));
|
|
184
|
+
note(dim(" this key can read diagrams but not change them — `flostep --help` for the rest."));
|
|
185
|
+
} else {
|
|
186
|
+
note(dim(` Next: ${cyan('echo "A -> B: hello" | flostep create --title "My flow" --share')}`));
|
|
187
|
+
note(dim(" steps go in on stdin, one per line — `flostep --help` for the rest."));
|
|
188
|
+
}
|
|
158
189
|
return 0;
|
|
159
190
|
}
|
|
160
191
|
|
|
192
|
+
// "team", or "team (complimentary until 10 Oct 2027)" while a comp is what's
|
|
193
|
+
// paying for it — the billing page calls it Complimentary and states the end,
|
|
194
|
+
// and a terminal saying only "team" would make that end a surprise.
|
|
195
|
+
export function formatPlan(workspace) {
|
|
196
|
+
return workspace.comped_until
|
|
197
|
+
? `${workspace.plan} (complimentary until ${formatExpiry(workspace.comped_until)})`
|
|
198
|
+
: workspace.plan;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function formatAccess(access) {
|
|
202
|
+
return access === "read" ? "Read only" : "Read & write";
|
|
203
|
+
}
|
|
204
|
+
|
|
161
205
|
// A named month rather than toLocaleDateString()'s default, which renders
|
|
162
206
|
// "28/11/2026" — unreadable without knowing whether the locale puts the day or
|
|
163
207
|
// the month first, and this line is the one thing a person reads about how long
|
package/src/commands/whoami.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveToken } from "../config.js";
|
|
2
2
|
import { CliError } from "../errors.js";
|
|
3
3
|
import { out, json, dim, bold } from "../output.js";
|
|
4
|
+
import { formatAccess, formatPlan } from "./login.js";
|
|
4
5
|
|
|
5
6
|
export default {
|
|
6
7
|
name: "whoami",
|
|
@@ -27,7 +28,9 @@ export default {
|
|
|
27
28
|
|
|
28
29
|
out(`${bold(me.email)}${me.name && me.name !== me.email ? dim(` (${me.name})`) : ""}`);
|
|
29
30
|
out(`${dim("Workspace")} ${me.workspace.name}${me.workspace.team ? dim(" (team)") : ""}`);
|
|
30
|
-
out(`${dim("Plan")} ${me.workspace
|
|
31
|
+
out(`${dim("Plan")} ${formatPlan(me.workspace)}`);
|
|
32
|
+
// Absent from servers before access levels, where every key could write.
|
|
33
|
+
if (me.access) out(`${dim("Access")} ${formatAccess(me.access)}`);
|
|
31
34
|
out(
|
|
32
35
|
`${dim("Diagrams")} ${
|
|
33
36
|
me.diagrams_remaining === null ? "unlimited" : `${me.diagrams_remaining} remaining on this plan`
|