flostep 0.1.1 → 0.1.3

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
@@ -48,6 +48,12 @@ npx flostep share 42 # prints the public link
48
48
  npx flostep share 42 --embed # iframe URL, for a docs page
49
49
  ```
50
50
 
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
+
53
+ ```bash
54
+ npx flostep share 42 --markdown # an image that follows the diagram as it changes
55
+ ```
56
+
51
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:
52
58
 
53
59
  ```bash
@@ -100,6 +106,7 @@ npx flostep whoami --json # which workspace you're writing to
100
106
  - **Don't leave files behind.** No command writes to disk. Redirect `show` yourself if the user asks for the steps in a file.
101
107
  - **Never invent a diagram id.** Get it from `list`, `create`, or the user.
102
108
  - **Exit codes**: `0` success, `1` error, `2` usage. Errors go to stderr with the reason.
109
+ - **Every command takes `--json`**, and `flostep help <command>` lists its flags — look them up rather than guess.
103
110
  - **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
111
  - **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
112
  - Ask before `delete`. It needs `--yes` when there's no terminal, and it cannot be undone.
package/README.md CHANGED
@@ -59,7 +59,7 @@ Run `flostep syntax` for the authoritative version, fetched from the server.
59
59
  | `flostep show <id>` | print a diagram as steps (pipeable) |
60
60
  | `flostep create` | create from stdin, writing no files (`--share` for a link) |
61
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) |
62
+ | `flostep share <id>` | public link on/off (`--embed` for an iframe, `--markdown` for a README image) |
63
63
  | `flostep open <id>` | open the editor in a browser |
64
64
  | `flostep delete <id>` | delete a diagram |
65
65
  | `flostep folder list\|create\|rename\|delete` | manage the workspace's folders |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flostep",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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/cli.js CHANGED
@@ -39,12 +39,22 @@ const GLOBAL_OPTIONS = {
39
39
  version: { type: "boolean", short: "v", default: false }
40
40
  };
41
41
 
42
+ const GLOBAL_OPTION_HELP = [
43
+ ["--json", "machine-readable output"],
44
+ ["-h, --help", "show this help"]
45
+ ];
46
+
42
47
 
43
48
  async function load(name) {
44
49
  const mod = await COMMANDS[name]();
45
50
  return mod.default;
46
51
  }
47
52
 
53
+ // Continuation lines line up under the first, after "Usage: ".
54
+ function usageText(command) {
55
+ return [].concat(command.usage).join("\n ");
56
+ }
57
+
48
58
  // The tool's own description of its surface, built from the very objects
49
59
  // `main` dispatches — so it cannot disagree with what the CLI actually accepts.
50
60
  //
@@ -95,10 +105,18 @@ function topLevelHelp() {
95
105
  out(" -v, --version print the version");
96
106
  out();
97
107
  out(bold("GETTING STARTED"));
98
- out(` ${cyan("flostep login")} sign in from a browser`);
99
- out(` ${cyan("flostep create --title X --share")} from stdin, no files, returns a link`);
100
- out(` ${cyan("flostep show 42 | flostep update 42")} read it, change it, write it back`);
101
- out(` ${cyan("flostep init")} teach this repo's coding agent to use flostep`);
108
+ // Each line has to run exactly as printed — `create` with nothing piped in
109
+ // only errors, so its example carries a step.
110
+ const starters = [
111
+ ["flostep login", "sign in from a browser"],
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"],
114
+ ["flostep init", "teach this repo's coding agent to use flostep"]
115
+ ];
116
+ const starterWidth = Math.max(...starters.map(([command]) => command.length));
117
+ for (const [command, what] of starters) {
118
+ out(` ${cyan(command.padEnd(starterWidth))} ${what}`);
119
+ }
102
120
  out();
103
121
  out(dim("Steps are written one per line: `From -> To: what happens`."));
104
122
  out(dim("Run `flostep syntax` for the full grammar, straight from the server."));
@@ -139,12 +157,13 @@ function commandHelp(command) {
139
157
  for (const line of [].concat(command.details)) out(` ${line}`);
140
158
  }
141
159
 
142
- if (command.optionHelp) {
143
- out();
144
- out(bold("OPTIONS"));
145
- for (const [flag, description] of command.optionHelp) {
146
- out(` ${flag.padEnd(22)} ${description}`);
147
- }
160
+ // The global flags are listed on every command, because every command
161
+ // honours them — a usage line that mentions --json on some commands and not
162
+ // others reads as though the rest don't take it.
163
+ out();
164
+ out(bold("OPTIONS"));
165
+ for (const [flag, description] of [ ...(command.optionHelp ?? []), ...GLOBAL_OPTION_HELP ]) {
166
+ out(` ${flag.padEnd(22)} ${description}`);
148
167
  }
149
168
 
150
169
  if (command.examples) {
@@ -162,6 +181,14 @@ export async function main(argv) {
162
181
 
163
182
  const wantsJson = argv.includes("--json");
164
183
 
184
+ // `flostep help create` is the form people try first, so it means the same
185
+ // as `flostep create --help`.
186
+ if (name === "help" && rest[0] && !rest[0].startsWith("-")) {
187
+ if (!Object.hasOwn(COMMANDS, rest[0])) throw unknownCommand(rest[0]);
188
+ commandHelp(await load(rest[0]));
189
+ return 0;
190
+ }
191
+
165
192
  if (!name || name === "help") {
166
193
  // --help is not an error; asking for it should exit 0 so a wrapper script
167
194
  // can call it without tripping `set -e`.
@@ -191,11 +218,7 @@ export async function main(argv) {
191
218
  return 0;
192
219
  }
193
220
 
194
- if (!Object.hasOwn(COMMANDS, name)) {
195
- throw new UsageError(`Unknown command "${name}".`, {
196
- usage: "Run `flostep --help` to see the available commands."
197
- });
198
- }
221
+ if (!Object.hasOwn(COMMANDS, name)) throw unknownCommand(name);
199
222
 
200
223
  const command = await load(name);
201
224
 
@@ -211,7 +234,7 @@ export async function main(argv) {
211
234
  strict: true
212
235
  });
213
236
  } catch (cause) {
214
- throw new UsageError(cause.message, { usage: [].concat(command.usage).join("\n") });
237
+ throw new UsageError(parseErrorMessage(cause), { usage: usageText(command) });
215
238
  }
216
239
 
217
240
  if (parsed.values.help) {
@@ -219,6 +242,12 @@ export async function main(argv) {
219
242
  return 0;
220
243
  }
221
244
 
245
+ // Honoured after a command too, as --help is, rather than parsed and ignored.
246
+ if (parsed.values.version) {
247
+ out(VERSION);
248
+ return 0;
249
+ }
250
+
222
251
  const { token, source: tokenSource } = resolveToken();
223
252
  const ctx = {
224
253
  json: parsed.values.json,
@@ -230,7 +259,31 @@ export async function main(argv) {
230
259
  }
231
260
  };
232
261
 
233
- return (await command.run({ positionals: parsed.positionals, values: parsed.values, ctx })) ?? 0;
262
+ try {
263
+ return (await command.run({ positionals: parsed.positionals, values: parsed.values, ctx })) ?? 0;
264
+ } catch (error) {
265
+ // Shared helpers like resolveTarget don't know which command called them;
266
+ // this does, so their usage errors still end with the right usage line.
267
+ if (error instanceof UsageError && !error.usage) error.usage = usageText(command);
268
+ throw error;
269
+ }
270
+ }
271
+
272
+ function unknownCommand(name) {
273
+ return new UsageError(`Unknown command "${name}".`, {
274
+ hint: "Run `flostep --help` to see the available commands."
275
+ });
276
+ }
277
+
278
+ // Node's own wording for a mistyped flag goes on to explain how to pass a
279
+ // positional that starts with "-", which is never what someone typing --tittle
280
+ // meant. Everything else it says is clear enough to keep.
281
+ function parseErrorMessage(cause) {
282
+ if (cause.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") {
283
+ const flag = cause.message.match(/'([^']+)'/)?.[1];
284
+ if (flag) return `Unknown option ${flag}.`;
285
+ }
286
+ return cause.message;
234
287
  }
235
288
 
236
289
  export async function run(argv) {
@@ -238,7 +291,7 @@ export async function run(argv) {
238
291
  return await main(argv);
239
292
  } catch (error) {
240
293
  if (error instanceof UsageError) {
241
- fail(error.message);
294
+ fail(error.message, error.hint);
242
295
  if (error.usage) {
243
296
  note();
244
297
  note(`Usage: ${error.usage}`);
@@ -11,7 +11,7 @@ import { diagramFromStdin, describe } from "../source.js";
11
11
  export default {
12
12
  name: "create",
13
13
  usage: [
14
- 'flostep create [--title <title>] [--share]',
14
+ 'flostep create [--title <title>] [--share] < steps.txt',
15
15
  'flostep show 42 | flostep create --title "Copy"'
16
16
  ],
17
17
  details: [
@@ -15,7 +15,7 @@ const SUBCOMMANDS = ["list", "create", "rename", "delete"];
15
15
  export default {
16
16
  name: "folder",
17
17
  usage: [
18
- "flostep folder list [--json]",
18
+ "flostep folder list",
19
19
  'flostep folder create "<name>"',
20
20
  'flostep folder rename "<old>" "<new>"',
21
21
  'flostep folder delete "<name>" [--yes]'
@@ -46,7 +46,7 @@ export default {
46
46
  ["-y, --yes", "skip the confirmation prompt"]
47
47
  ],
48
48
  examples: [
49
- ["npx flostep init", "in the root of the repository"],
49
+ ["flostep init", "in the root of the repository"],
50
50
  ["flostep init --file docs/agents.md"],
51
51
  ["flostep init --print >> system-prompt.md", "for a prompt that isn't a file in a repo"]
52
52
  ],
@@ -2,7 +2,7 @@ import { out, table, json, relativeTime, dim } from "../output.js";
2
2
 
3
3
  export default {
4
4
  name: "list",
5
- usage: "flostep list [--folder <name>] [--json]",
5
+ usage: "flostep list [--folder <name>]",
6
6
  details: [
7
7
  "Every diagram in the workspace the key belongs to, most recently updated first.",
8
8
  "--folder narrows it to one folder; `--folder uncategorized` to diagrams in none."
@@ -39,7 +39,7 @@ export default {
39
39
 
40
40
  if (diagrams.length === 0) {
41
41
  out(dim(folder === undefined
42
- ? "No diagrams yet. Create one with `flostep create --title X --share`."
42
+ ? "No diagrams yet. Create one with `echo 'A -> B: hello' | flostep create --title X --share`."
43
43
  : "No diagrams in that folder."));
44
44
  return 0;
45
45
  }
@@ -13,7 +13,7 @@ import { hostname } from "node:os";
13
13
 
14
14
  import { CliError } from "../errors.js";
15
15
  import { readConfig, writeConfig, resolveHost } from "../config.js";
16
- import { note, json, box, fields, startSpinner, dim, bold, cyan, green } from "../output.js";
16
+ import { note, json, box, banner, fields, startSpinner, dim, bold, cyan, green } from "../output.js";
17
17
  import { Api } from "../api.js";
18
18
  import { openBrowser } from "../browser.js";
19
19
 
@@ -46,6 +46,8 @@ export default {
46
46
  async run({ values, ctx }) {
47
47
  const host = resolveHost();
48
48
 
49
+ if (!ctx.json) banner();
50
+
49
51
  if (values.token) return storeToken({ host, token: values.token.trim(), ctx });
50
52
 
51
53
  const api = ctx.api();
@@ -151,8 +153,8 @@ async function storeToken({ host, token, expiresAt, ctx }) {
151
153
  ...(expiresAt ? [ [ "Expires", formatExpiry(expiresAt) ] ] : [])
152
154
  ]);
153
155
  note();
154
- note(dim(` Next: ${cyan('flostep create --title "My flow" --share')}`));
155
- note(dim(" steps on stdin, no files — or `flostep --help` for the rest."));
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."));
156
158
  return 0;
157
159
  }
158
160
 
@@ -21,7 +21,7 @@ export default {
21
21
  name: "node",
22
22
  usage: [
23
23
  'flostep node rename <id> "<old>" "<new>"',
24
- "flostep node list <id> [--json]"
24
+ "flostep node list <id>"
25
25
  ],
26
26
  details: [
27
27
  "Components are identified by name, case-insensitively, so a rename changes",
@@ -1,4 +1,4 @@
1
- import { out, ok, note, dim } from "../output.js";
1
+ import { out, ok, note, json, dim } from "../output.js";
2
2
  import { openBrowser } from "../browser.js";
3
3
  import { resolveTarget } from "../target.js";
4
4
 
@@ -8,7 +8,7 @@ export default {
8
8
  details: ["Opens the diagram in the editor in your default browser."],
9
9
  examples: [
10
10
  ["flostep open 15"],
11
- ["flostep open 42"]
11
+ ["flostep open 15 --json", "the url, and whether a browser started"]
12
12
  ],
13
13
 
14
14
  async run({ positionals, ctx }) {
@@ -17,7 +17,14 @@ export default {
17
17
 
18
18
  // Awaited: whether a browser actually started is only known a tick later,
19
19
  // and headless is a normal place to run this — the URL is the useful half.
20
- if (await openBrowser(diagram.url)) {
20
+ const opened = await openBrowser(diagram.url);
21
+
22
+ if (ctx.json) {
23
+ json({ id: diagram.id, title: diagram.title, url: diagram.url, opened });
24
+ return 0;
25
+ }
26
+
27
+ if (opened) {
21
28
  ok(`Opening ${diagram.title}`);
22
29
  out(diagram.url);
23
30
  } else {
@@ -1,24 +1,56 @@
1
1
  import { out, ok, json, dim } from "../output.js";
2
2
  import { resolveTarget } from "../target.js";
3
+ import { CliError, UsageError } from "../errors.js";
4
+
5
+ // --off on a line of its own, because it takes neither of the others.
6
+ const USAGE = [
7
+ "flostep share <id> [--embed | --markdown]",
8
+ "flostep share <id> --off"
9
+ ];
10
+
11
+ // Square brackets would end the alt text early, so the title gives them up.
12
+ function markdown(title, imageUrl, linkUrl) {
13
+ const alt = String(title ?? "").replace(/[\[\]\s]+/g, " ").trim() || "Diagram";
14
+ return `[![${alt}](${imageUrl})](${linkUrl})`;
15
+ }
16
+
17
+ // The server decides what a plan includes and says so with a null; the CLI
18
+ // never restates the plan rules. A missing key is a different thing — a server
19
+ // from before the feature — and saying "upgrade" there would be wrong.
20
+ function need(value, what, shareUrl) {
21
+ if (value === undefined) {
22
+ throw new CliError(`This Flostep server doesn't offer ${what} yet.`);
23
+ }
24
+ if (value === null) {
25
+ throw new CliError(`${what[0].toUpperCase()}${what.slice(1)} need a paid plan.`, {
26
+ hint: `The diagram is shared, and the link works on every plan: ${shareUrl}`
27
+ });
28
+ }
29
+ return value;
30
+ }
3
31
 
4
32
  export default {
5
33
  name: "share",
6
- usage: "flostep share <id> [--off] [--embed]",
34
+ usage: USAGE,
7
35
  details: [
8
36
  "Turns on the public link and prints it — the natural last step after `create` or `update`.",
9
- "The link is read-only and needs no account; viewers can step through the flow."
37
+ "The link is read-only and needs no account; viewers can step through the flow.",
38
+ "GitHub strips iframes, so for a README, an ADR or a PR use --markdown: an image of the diagram that links to the walkthrough."
10
39
  ],
11
40
  options: {
12
41
  off: { type: "boolean", default: false },
13
- embed: { type: "boolean", default: false }
42
+ embed: { type: "boolean", default: false },
43
+ markdown: { type: "boolean", default: false }
14
44
  },
15
45
  optionHelp: [
16
46
  ["--off", "stop sharing (the URL is kept, so re-sharing restores it)"],
17
- ["--embed", "print the iframe URL instead of the page URL"]
47
+ ["--embed", "print the iframe URL instead of the page URL"],
48
+ ["--markdown", "print a markdown image that links to the diagram (paid plans)"]
18
49
  ],
19
50
  examples: [
20
51
  ["flostep share 15"],
21
- ["flostep share 42 --embed", "for a README or Confluence page"],
52
+ ["flostep share 42 --embed", "for a docs page or Confluence"],
53
+ ["flostep share 42 --markdown", "for a GitHub README; follows the diagram"],
22
54
  ["flostep share 15 --off"]
23
55
  ],
24
56
 
@@ -26,22 +58,40 @@ export default {
26
58
  const { id } = resolveTarget(positionals[0]);
27
59
  const api = ctx.api();
28
60
 
29
- const result = values.off ? await api.unshareDiagram(id) : await api.shareDiagram(id);
30
-
31
- if (ctx.json) {
32
- json(result);
33
- return 0;
61
+ if (values.off && (values.embed || values.markdown)) {
62
+ throw new UsageError("--off takes no other flag.", { usage: USAGE.join("\n ") });
34
63
  }
64
+ if (values.embed && values.markdown) {
65
+ throw new UsageError("Use --embed or --markdown, not both.", { usage: USAGE.join("\n ") });
66
+ }
67
+
68
+ const result = values.off ? await api.unshareDiagram(id) : await api.shareDiagram(id);
35
69
 
36
70
  if (values.off) {
71
+ if (ctx.json) {
72
+ json(result);
73
+ return 0;
74
+ }
37
75
  ok(`Sharing off for #${result.id} ${dim(result.title)}`);
38
76
  // Worth saying: people expect "unshare" to burn the URL, and it doesn't.
39
77
  out(dim("The link is kept — sharing again restores the same URL."));
40
78
  return 0;
41
79
  }
42
80
 
81
+ // Checked before anything is printed, --json included: what was asked for
82
+ // either exists or the command fails, in both registers.
83
+ if (values.markdown) need(result.image_url, "image embeds", result.share_url);
84
+
85
+ const snippet = values.markdown ? markdown(result.title, result.image_url, result.share_url) : null;
86
+
87
+ if (ctx.json) {
88
+ json(snippet ? { ...result, markdown: snippet } : result);
89
+ return 0;
90
+ }
91
+
43
92
  ok(`Sharing #${result.id} ${dim(result.title)}`);
44
- out(values.embed ? result.embed_url : result.share_url);
93
+ if (snippet) out(snippet);
94
+ else out(values.embed ? result.embed_url : result.share_url);
45
95
  return 0;
46
96
  }
47
97
  };
@@ -19,7 +19,7 @@ export default {
19
19
  usage: [
20
20
  'flostep step add <id> "From -> To: what happens" [--at <n>]',
21
21
  "flostep step rm <id> <n>",
22
- "flostep step list <id> [--json]"
22
+ "flostep step list <id>"
23
23
  ],
24
24
  details: [
25
25
  "Steps are numbered from 1, matching the badges on the canvas.",
@@ -77,7 +77,7 @@ export default {
77
77
  function applyAdd({ code, text, at }) {
78
78
  if (!text.trim()) {
79
79
  throw new UsageError('What step? Pass it in quotes: "From -> To: what happens".', {
80
- usage: 'flostep step add <id> "From -> To: what happens"'
80
+ usage: 'flostep step add <id> "From -> To: what happens" [--at <n>]'
81
81
  });
82
82
  }
83
83
 
@@ -85,7 +85,7 @@ function applyAdd({ code, text, at }) {
85
85
  if (at !== undefined) {
86
86
  position = Number(at);
87
87
  if (!Number.isInteger(position)) {
88
- throw new UsageError("--at takes a step number.", { usage: "flostep step add <id> <step> --at <n>" });
88
+ throw new UsageError("--at takes a step number.", { usage: 'flostep step add <id> "From -> To: what happens" --at <n>' });
89
89
  }
90
90
  }
91
91
 
@@ -1,4 +1,4 @@
1
- import { out } from "../output.js";
1
+ import { out, json } from "../output.js";
2
2
 
3
3
  export default {
4
4
  name: "syntax",
@@ -11,7 +11,8 @@ export default {
11
11
 
12
12
  async run({ ctx }) {
13
13
  const { syntax } = await ctx.api().syntax();
14
- out(syntax);
14
+ if (ctx.json) json({ syntax });
15
+ else out(syntax);
15
16
  return 0;
16
17
  }
17
18
  };
@@ -13,7 +13,7 @@ import { diagramFromStdin, describe } from "../source.js";
13
13
  export default {
14
14
  name: "update",
15
15
  usage: [
16
- "flostep update <id> [--title <title>] [--if-version <n>]",
16
+ "flostep update <id> [--title <title>] [--if-version <n>] < steps.txt",
17
17
  "flostep show 42 | flostep update 42"
18
18
  ],
19
19
  details: [
@@ -7,7 +7,7 @@ export default {
7
7
  usage: "flostep whoami",
8
8
  details: [
9
9
  "A key belongs to you but acts on the whole workspace — anything it creates",
10
- "belongs to the team. This is how you check which library you're writing to."
10
+ "belongs to the team. This is how you check which workspace you're writing to."
11
11
  ],
12
12
  examples: [
13
13
  ["flostep whoami"],
package/src/errors.js CHANGED
@@ -17,10 +17,11 @@ export class CliError extends Error {
17
17
  }
18
18
 
19
19
  export class UsageError extends Error {
20
- constructor(message, { usage } = {}) {
20
+ constructor(message, { usage, hint } = {}) {
21
21
  super(message);
22
22
  this.name = "UsageError";
23
23
  this.exitCode = 2;
24
24
  this.usage = usage;
25
+ this.hint = hint;
25
26
  }
26
27
  }
package/src/output.js CHANGED
@@ -118,6 +118,24 @@ export function box(lines, { indent = " " } = {}) {
118
118
  note(`${indent}${dim("╰" + bar + "╯")}`);
119
119
  }
120
120
 
121
+ // The wordmark, shown once at the top of a login. Skipped when stderr is not a
122
+ // TTY — in a CI log it is noise — and when the terminal is too
123
+ // narrow, where it would wrap into an unreadable mess.
124
+ const WORDMARK = [
125
+ "█▀▀ █ █▀█ █▀ ▀█▀ █▀▀ █▀█",
126
+ "█▀ █▄▄ █▄█ ▄█ █ ██▄ █▀▀"
127
+ ];
128
+
129
+ export function banner({ indent = " " } = {}) {
130
+ if (!process.stderr.isTTY) return;
131
+
132
+ const width = indent.length + Math.max(...WORDMARK.map((line) => line.length));
133
+ if ((process.stderr.columns ?? 80) < width) return;
134
+
135
+ note();
136
+ for (const line of WORDMARK) note(`${indent}${cyan(line)}`);
137
+ }
138
+
121
139
  // Label/value rows, aligned. Same idea as `table` but for a single record.
122
140
  export function fields(rows, { indent = " " } = {}) {
123
141
  const width = Math.max(...rows.map(([label]) => label.length));
package/src/stdin.js CHANGED
@@ -12,7 +12,7 @@ export async function readStdin({ what }) {
12
12
  // staring at a cursor with no idea the process is waiting on them.
13
13
  if (process.stdin.isTTY) {
14
14
  throw new CliError(`No steps on stdin.`, {
15
- hint: `Pipe them in — ${what} — or redirect a file into it: \`flostep create < flow.txt\`.`
15
+ hint: `Pipe them in, or redirect a file: ${what}.`
16
16
  });
17
17
  }
18
18
 
package/src/target.js CHANGED
@@ -5,7 +5,7 @@
5
5
  // a pipe and write nothing to disk, so there is no local file to name and
6
6
  // nothing to keep in sync.
7
7
 
8
- import { CliError } from "./errors.js";
8
+ import { UsageError } from "./errors.js";
9
9
 
10
10
  // A bare positive integer. Ids come from `flostep list`, from `create`, or
11
11
  // from the user — never from a guess.
@@ -13,15 +13,18 @@ export function looksLikeId(ref) {
13
13
  return /^[1-9][0-9]*$/.test(String(ref).trim());
14
14
  }
15
15
 
16
+ // Usage errors, not CliErrors: a missing or malformed id is the caller getting
17
+ // the command wrong, and exits 2 like any other. cli.js fills in the usage line
18
+ // of whichever command was running.
16
19
  export function resolveTarget(ref) {
17
20
  if (!ref) {
18
- throw new CliError("Which diagram? Pass an id.", {
21
+ throw new UsageError("Which diagram? Pass an id.", {
19
22
  hint: "Run `flostep list` to see ids."
20
23
  });
21
24
  }
22
25
 
23
26
  if (!looksLikeId(ref)) {
24
- throw new CliError(`"${ref}" is not a diagram id.`, {
27
+ throw new UsageError(`"${ref}" is not a diagram id.`, {
25
28
  hint: "Ids are numbers — run `flostep list` to see them."
26
29
  });
27
30
  }