flostep 0.1.3 → 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/AGENTS.md CHANGED
@@ -35,7 +35,7 @@ Customer -> API: POST /checkout
35
35
  API -> Payments: charge card
36
36
  API -> Customer: order confirmed
37
37
  EOF
38
- # ✓ Created #42 Checkout
38
+ # ✓ Created #Xk3p9QvA2wE Checkout
39
39
  # https://flostep.dev/s/rEFdW8GSDwQ <- give the user this
40
40
  ```
41
41
 
@@ -44,36 +44,36 @@ EOF
44
44
  For a diagram that already exists, share it on its own:
45
45
 
46
46
  ```bash
47
- npx flostep share 42 # prints the public link
48
- npx flostep share 42 --embed # iframe URL, for a docs page
47
+ npx flostep share Xk3p9QvA2wE # prints the public link
48
+ npx flostep share Xk3p9QvA2wE --embed # iframe URL, for a docs page
49
49
  ```
50
50
 
51
51
  GitHub strips iframes, so for a README, an ADR or a PR description print a markdown image that links to the diagram instead (paid plans; on a free one the command fails and gives you the plain link):
52
52
 
53
53
  ```bash
54
- npx flostep share 42 --markdown # an image that follows the diagram as it changes
54
+ npx flostep share Xk3p9QvA2wE --markdown # an image that follows the diagram as it changes
55
55
  ```
56
56
 
57
57
  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:
58
58
 
59
59
  ```bash
60
- npx flostep update 42 < docs/checkout.flostep
60
+ npx flostep update Xk3p9QvA2wE < docs/checkout.flostep
61
61
  ```
62
62
 
63
63
  To rewrite an existing diagram, read it, transform it, pipe it back:
64
64
 
65
65
  ```bash
66
- npx flostep show 42 | sed 's/Redis/Session Cache/' | npx flostep update 42
66
+ npx flostep show Xk3p9QvA2wE | sed 's/Redis/Session Cache/' | npx flostep update Xk3p9QvA2wE
67
67
  ```
68
68
 
69
69
  Or change one step at a time when you have no local file:
70
70
 
71
71
  ```bash
72
- npx flostep show 42 # read it first
73
- npx flostep step add 42 "API -> Cache: read session"
74
- npx flostep step add 42 "Client -> API: retry" --at 3
75
- npx flostep step rm 42 5
76
- npx flostep node rename 42 "Redis" "Session Cache"
72
+ npx flostep show Xk3p9QvA2wE # read it first
73
+ npx flostep step add Xk3p9QvA2wE "API -> Cache: read session"
74
+ npx flostep step add Xk3p9QvA2wE "Client -> API: retry" --at 3
75
+ npx flostep step rm Xk3p9QvA2wE 5
76
+ npx flostep node rename Xk3p9QvA2wE "Redis" "Session Cache"
77
77
  ```
78
78
 
79
79
  ### Folders
@@ -81,9 +81,9 @@ npx flostep node rename 42 "Redis" "Session Cache"
81
81
  Folders are shared by a team and addressed by name. File a diagram only when the user asks you to organise it:
82
82
 
83
83
  ```bash
84
- npx flostep folder list --json # names and diagram counts
85
- npx flostep folder create "Payments" # returns the folder if it already exists
86
- npx flostep move 42 "Payments" # file it; --none takes it out
84
+ npx flostep folder list --json # names and diagram counts
85
+ npx flostep folder create "Payments" # returns the folder if it already exists
86
+ npx flostep move Xk3p9QvA2wE "Payments" # file it; --none takes it out
87
87
  npx flostep list --folder "Payments" --json
88
88
  ```
89
89
 
@@ -92,11 +92,11 @@ npx flostep list --folder "Payments" --json
92
92
  ### Reading
93
93
 
94
94
  ```bash
95
- npx flostep list --json # ids, titles, urls
96
- npx flostep show 42 # the steps, plain text
97
- npx flostep step list 42 --json # numbered steps
98
- npx flostep node list 42 --json # components, first-appearance order
99
- npx flostep whoami --json # which workspace you're writing to
95
+ npx flostep list --json # ids, titles, urls
96
+ npx flostep show Xk3p9QvA2wE # the steps, plain text
97
+ npx flostep step list Xk3p9QvA2wE --json # numbered steps
98
+ npx flostep node list Xk3p9QvA2wE --json # components, first-appearance order
99
+ npx flostep whoami --json # which workspace you're writing to
100
100
  ```
101
101
 
102
102
  ### Rules
package/README.md CHANGED
@@ -20,14 +20,14 @@ Customer -> API: POST /checkout
20
20
  API -> Payments: charge card
21
21
  API -> Customer: order confirmed
22
22
  EOF
23
- # ✓ Created #42 Checkout
23
+ # ✓ Created #Xk3p9QvA2wE Checkout
24
24
  # https://flostep.dev/s/rEFdW8GSDwQ
25
25
  ```
26
26
 
27
27
  That wrote nothing to disk, and the id it printed is what every other command takes:
28
28
 
29
29
  ```bash
30
- npx flostep show 42 | sed 's/Payments/Stripe/' | npx flostep update 42
30
+ npx flostep show Xk3p9QvA2wE | sed 's/Payments/Stripe/' | npx flostep update Xk3p9QvA2wE
31
31
  ```
32
32
 
33
33
  ## The format
@@ -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) |
@@ -79,7 +79,7 @@ Ids come from `flostep list` or from `create`. Nothing is resolved from a file p
79
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
80
 
81
81
  ```yaml
82
- - run: npx flostep update 42 < docs/checkout.flostep
82
+ - run: npx flostep update Xk3p9QvA2wE < docs/checkout.flostep
83
83
  env:
84
84
  FLOSTEP_TOKEN: ${{ secrets.FLOSTEP_TOKEN }}
85
85
  ```
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flostep",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Build, update and share Flostep diagrams from the terminal, from CI, or from a coding agent.",
5
5
  "type": "module",
6
6
  "bin": {
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
- requestDeviceCode(clientName) {
190
- return this.post("/api/v1/device/code", { client_name: clientName });
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/cli.js CHANGED
@@ -110,7 +110,7 @@ function topLevelHelp() {
110
110
  const starters = [
111
111
  ["flostep login", "sign in from a browser"],
112
112
  ['echo "A -> B: hello" | flostep create --share', "steps on stdin, returns a link"],
113
- ["flostep show 42 | flostep update 42", "read it, change it, write it back"],
113
+ ["flostep show Xk3p9QvA2wE | flostep update Xk3p9QvA2wE", "read it, change it, write it back"],
114
114
  ["flostep init", "teach this repo's coding agent to use flostep"]
115
115
  ];
116
116
  const starterWidth = Math.max(...starters.map(([command]) => command.length));
@@ -12,7 +12,7 @@ export default {
12
12
  name: "create",
13
13
  usage: [
14
14
  'flostep create [--title <title>] [--share] < steps.txt',
15
- 'flostep show 42 | flostep create --title "Copy"'
15
+ 'flostep show Xk3p9QvA2wE | flostep create --title "Copy"'
16
16
  ],
17
17
  details: [
18
18
  "Reads steps from stdin and creates a diagram. Writes no files.",
@@ -13,8 +13,8 @@ export default {
13
13
  options: { yes: { type: "boolean", short: "y", default: false } },
14
14
  optionHelp: [["-y, --yes", "skip the confirmation prompt"]],
15
15
  examples: [
16
- ["flostep delete 15"],
17
- ["flostep delete 15 --yes", "for scripts and agents"]
16
+ ["flostep delete Xk3p9QvA2wE"],
17
+ ["flostep delete Xk3p9QvA2wE --yes", "for scripts and agents"]
18
18
  ],
19
19
 
20
20
  async run({ positionals, values, ctx }) {
@@ -38,14 +38,17 @@ export default {
38
38
  }
39
39
 
40
40
  if (diagrams.length === 0) {
41
- out(dim(folder === undefined
42
- ? "No diagrams yet. Create one with `echo 'A -> B: hello' | flostep create --title X --share`."
43
- : "No diagrams in that folder."));
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
 
47
50
  table(diagrams, [
48
- ["ID", (d) => d.id, "right"],
51
+ ["ID", (d) => d.id],
49
52
  ["TITLE", (d) => d.title],
50
53
  ["FOLDER", (d) => d.folder ?? ""],
51
54
  ["UPDATED", (d) => relativeTime(d.updated_at)],
@@ -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.plan ],
176
+ [ "Plan", formatPlan(me.workspace) ],
177
+ ...(me.access ? [ [ "Access", formatAccess(me.access) ] ] : []),
153
178
  ...(expiresAt ? [ [ "Expires", formatExpiry(expiresAt) ] ] : [])
154
179
  ]);
155
180
  note();
156
- note(dim(` Next: ${cyan('echo "A -> B: hello" | flostep create --title "My flow" --share')}`));
157
- note(dim(" steps go in on stdin, one per line — `flostep --help` for the rest."));
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
@@ -17,8 +17,8 @@ export default {
17
17
  options: { none: { type: "boolean", default: false } },
18
18
  optionHelp: [["--none", "take the diagram out of its folder (uncategorized)"]],
19
19
  examples: [
20
- ['flostep move 42 "Payments"'],
21
- ["flostep move 42 --none"]
20
+ ['flostep move Xk3p9QvA2wE "Payments"'],
21
+ ["flostep move Xk3p9QvA2wE --none"]
22
22
  ],
23
23
 
24
24
  async run({ positionals, values, ctx }) {
@@ -7,7 +7,7 @@
7
7
  // way to express a type — the server infers that from the name. A `--type cache`
8
8
  // flag would be discarded on the next write, which is worse than not offering it.
9
9
  //
10
- // To add a component, name it in a step: `flostep step add 15 "API -> Redis: read"`.
10
+ // To add a component, name it in a step: `flostep step add Xk3p9QvA2wE "API -> Redis: read"`.
11
11
 
12
12
 
13
13
  import { UsageError } from "../errors.js";
@@ -29,9 +29,9 @@ export default {
29
29
  "Types (service, database, person…) are inferred from the name by the server."
30
30
  ],
31
31
  examples: [
32
- ["flostep node list 15"],
33
- ['flostep node rename 15 "Redis" "Session Cache"'],
34
- ["flostep node list 15 --json", "components in first-appearance order"]
32
+ ["flostep node list Xk3p9QvA2wE"],
33
+ ['flostep node rename Xk3p9QvA2wE "Redis" "Session Cache"'],
34
+ ["flostep node list Xk3p9QvA2wE --json", "components in first-appearance order"]
35
35
  ],
36
36
 
37
37
  async run({ positionals, ctx }) {
@@ -7,8 +7,8 @@ export default {
7
7
  usage: "flostep open <id>",
8
8
  details: ["Opens the diagram in the editor in your default browser."],
9
9
  examples: [
10
- ["flostep open 15"],
11
- ["flostep open 15 --json", "the url, and whether a browser started"]
10
+ ["flostep open Xk3p9QvA2wE"],
11
+ ["flostep open Xk3p9QvA2wE --json", "the url, and whether a browser started"]
12
12
  ],
13
13
 
14
14
  async run({ positionals, ctx }) {
@@ -48,10 +48,10 @@ export default {
48
48
  ["--markdown", "print a markdown image that links to the diagram (paid plans)"]
49
49
  ],
50
50
  examples: [
51
- ["flostep share 15"],
52
- ["flostep share 42 --embed", "for a docs page or Confluence"],
53
- ["flostep share 42 --markdown", "for a GitHub README; follows the diagram"],
54
- ["flostep share 15 --off"]
51
+ ["flostep share Xk3p9QvA2wE"],
52
+ ["flostep share Xk3p9QvA2wE --embed", "for a docs page or Confluence"],
53
+ ["flostep share Xk3p9QvA2wE --markdown", "for a GitHub README; follows the diagram"],
54
+ ["flostep share Xk3p9QvA2wE --off"]
55
55
  ],
56
56
 
57
57
  async run({ positionals, values, ctx }) {
@@ -6,12 +6,12 @@ export default {
6
6
  usage: "flostep show <id>",
7
7
  details: [
8
8
  "Prints the diagram as steps, and nothing else, so it pipes cleanly:",
9
- " flostep show 15 > docs/checkout.flostep"
9
+ " flostep show Xk3p9QvA2wE > docs/checkout.flostep"
10
10
  ],
11
11
  examples: [
12
- ["flostep show 15"],
13
- ["flostep show 42 > docs/checkout.flostep", "redirect it yourself if you want a file"],
14
- ["flostep show 15 --json", "with the title and url alongside the code"]
12
+ ["flostep show Xk3p9QvA2wE"],
13
+ ["flostep show Xk3p9QvA2wE > docs/checkout.flostep", "redirect it yourself if you want a file"],
14
+ ["flostep show Xk3p9QvA2wE --json", "with the title and url alongside the code"]
15
15
  ],
16
16
 
17
17
  async run({ positionals, ctx }) {
@@ -28,10 +28,10 @@ export default {
28
28
  options: { at: { type: "string" } },
29
29
  optionHelp: [["--at <n>", "insert before step n instead of appending"]],
30
30
  examples: [
31
- ['flostep step add 15 "API -> Cache: read session"'],
32
- ['flostep step add 15 "Client -> API: retry" --at 3'],
33
- ["flostep step list 15 --json"],
34
- ["flostep step rm 15 4"]
31
+ ['flostep step add Xk3p9QvA2wE "API -> Cache: read session"'],
32
+ ['flostep step add Xk3p9QvA2wE "Client -> API: retry" --at 3'],
33
+ ["flostep step list Xk3p9QvA2wE --json"],
34
+ ["flostep step rm Xk3p9QvA2wE 4"]
35
35
  ],
36
36
 
37
37
  async run({ positionals, values, ctx }) {
@@ -1,7 +1,7 @@
1
1
  // `flostep update` — replace a diagram's steps from a pipe.
2
2
  //
3
3
  // The counterpart to `create`, and the composable half of the tool:
4
- // flostep show 42 | sed 's/Redis/Session Cache/' | flostep update 42
4
+ // flostep show Xk3p9QvA2wE | sed 's/Redis/Session Cache/' | flostep update Xk3p9QvA2wE
5
5
 
6
6
  import { readStdin } from "../stdin.js";
7
7
  import { UsageError } from "../errors.js";
@@ -14,7 +14,7 @@ export default {
14
14
  name: "update",
15
15
  usage: [
16
16
  "flostep update <id> [--title <title>] [--if-version <n>] < steps.txt",
17
- "flostep show 42 | flostep update 42"
17
+ "flostep show Xk3p9QvA2wE | flostep update Xk3p9QvA2wE"
18
18
  ],
19
19
  details: [
20
20
  "Reads steps from stdin and replaces every step in the diagram.",
@@ -31,15 +31,15 @@ export default {
31
31
  ["--if-version <n>", "only write if the diagram is still at this version"]
32
32
  ],
33
33
  examples: [
34
- ["flostep show 42 | sed 's/Redis/Cache/' | flostep update 42"],
35
- ["flostep update 42 < revised.txt"],
36
- ["flostep update 42 --if-version 7 < revised.txt", "refused if it changed since version 7"]
34
+ ["flostep show Xk3p9QvA2wE | sed 's/Redis/Cache/' | flostep update Xk3p9QvA2wE"],
35
+ ["flostep update Xk3p9QvA2wE < revised.txt"],
36
+ ["flostep update Xk3p9QvA2wE --if-version 7 < revised.txt", "refused if it changed since version 7"]
37
37
  ],
38
38
 
39
39
  async run({ positionals, values, ctx }) {
40
40
  const { id } = resolveTarget(positionals[0]);
41
41
  const version = parseVersion(values["if-version"]);
42
- const input = await readStdin({ what: "`flostep update 42 < flow.txt`" });
42
+ const input = await readStdin({ what: "`flostep update Xk3p9QvA2wE < flow.txt`" });
43
43
  const { code, summary } = diagramFromStdin(input);
44
44
 
45
45
  const body = { code };
@@ -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.plan}`);
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`
package/src/target.js CHANGED
@@ -1,4 +1,4 @@
1
- // Turning "15" into a diagram id.
1
+ // Turning "Xk3_p9QvA2w" into a diagram id.
2
2
  //
3
3
  // Every command that acts on one diagram takes an id, and only an id. Diagrams
4
4
  // live in the account, not in a checkout: `create` and `update` read steps from
@@ -7,10 +7,13 @@
7
7
 
8
8
  import { UsageError } from "./errors.js";
9
9
 
10
- // A bare positive integer. Ids come from `flostep list`, from `create`, or
11
- // from the user — never from a guess.
10
+ // Any URL-safe token. Ids are random strings now; the integers servers used to
11
+ // hand out still resolve, so they pass too. This deliberately doesn't pin the
12
+ // server's exact format — when ids were checked as digits-only, changing the
13
+ // format would have broken every installed CLI. A wrong id is the server's 404
14
+ // to report; what this catches is a title passed where the id goes.
12
15
  export function looksLikeId(ref) {
13
- return /^[1-9][0-9]*$/.test(String(ref).trim());
16
+ return /^[A-Za-z0-9_-]+$/.test(String(ref).trim());
14
17
  }
15
18
 
16
19
  // Usage errors, not CliErrors: a missing or malformed id is the caller getting
@@ -25,9 +28,9 @@ export function resolveTarget(ref) {
25
28
 
26
29
  if (!looksLikeId(ref)) {
27
30
  throw new UsageError(`"${ref}" is not a diagram id.`, {
28
- hint: "Ids are numbers — run `flostep list` to see them."
31
+ hint: "Pass the ID column from `flostep list`, not the title."
29
32
  });
30
33
  }
31
34
 
32
- return { id: Number(ref) };
35
+ return { id: String(ref).trim() };
33
36
  }