@floomhq/floom 3.0.1 → 3.0.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/README.md ADDED
@@ -0,0 +1,168 @@
1
+ # @floomhq/floom
2
+
3
+ The Floom CLI: one shared skill library, synced across every AI agent.
4
+
5
+ Docs: <https://floom.dev/docs>
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm i -g @floomhq/floom
11
+ ```
12
+
13
+ Pinned no-install form for automation:
14
+
15
+ ```bash
16
+ npx -y @floomhq/floom@3.0.2 --help
17
+ ```
18
+
19
+ ## Login
20
+
21
+ ```bash
22
+ floom login
23
+ ```
24
+
25
+ The login command starts a browser device flow. Sign in at floom.dev with
26
+ Google or email, approve the device code, and the CLI stores the session at
27
+ `~/.floom/auth.json`.
28
+
29
+ Check the active account:
30
+
31
+ ```bash
32
+ floom account
33
+ ```
34
+
35
+ ## First Skill
36
+
37
+ ```bash
38
+ floom push ~/.claude/skills/my-skill
39
+ ```
40
+
41
+ The folder must contain `SKILL.md`. Each push creates a Library version.
42
+
43
+ Create a starter skill when the folder does not exist yet:
44
+
45
+ ```bash
46
+ floom new my-skill --agent claude
47
+ ```
48
+
49
+ ## Bulk Sync
50
+
51
+ ```bash
52
+ floom sync
53
+ ```
54
+
55
+ Zero-arg `floom sync` detects installed agents and supported scopes, reviews
56
+ local-to-Library and Library-to-local differences, and applies the selected
57
+ plan. Use `--dry-run` to preview the plan without changing files or Library
58
+ records.
59
+
60
+ Library-to-local only:
61
+
62
+ ```bash
63
+ floom pull
64
+ floom pull --agent claude,codex
65
+ ```
66
+
67
+ Local-to-Library only:
68
+
69
+ ```bash
70
+ floom push
71
+ ```
72
+
73
+ Use the installed `floom` command for interactive terminal work. Use the pinned
74
+ `npx -y @floomhq/floom@3.0.2 <command>` form for automation and agent-run
75
+ commands that need reproducible CLI behavior.
76
+
77
+ ## Releasing
78
+
79
+ Releases publish to npm and bump the Homebrew tap formula automatically via
80
+ `.github/workflows/publish-cli.yml`, triggered by a `cli-v*` tag.
81
+
82
+ ### Hard pre-publish smoke gate
83
+
84
+ Every `npm publish` is gated by `packages/cli/scripts/pre-publish-smoke.sh`,
85
+ which packs the tarball, installs it into an isolated `$HOME`, and drives
86
+ every subcommand the way a real user would. If anything fails, the workflow
87
+ fails BEFORE `npm publish` runs — no broken CLI can reach npm `latest`.
88
+
89
+ Background: in 2026-05 we shipped `3.0.0` → `3.0.1` → `3.0.2` within hours
90
+ because `3.0.0` hit npm `latest` with three P0s that code review missed:
91
+ `--help` ran the destructive body, `--help | head` threw an unhandled EPIPE,
92
+ and unknown commands silently rendered the dashboard. Only an out-of-band
93
+ real-terminal smoke caught them. This script is that smoke, baked into the
94
+ publish pipeline.
95
+
96
+ ### Before pushing a `cli-v*` tag
97
+
98
+ Run the local pre-tag check from the repo root:
99
+
100
+ ```bash
101
+ ./scripts/pre-tag-check.sh
102
+ ```
103
+
104
+ It:
105
+
106
+ 1. Confirms the working tree is clean.
107
+ 2. Rebuilds the CLI from a clean `dist/`.
108
+ 3. Runs the CLI unit test suite (`pnpm --filter @floomhq/floom test`).
109
+ 4. Runs the full pre-publish smoke (same script the workflow runs).
110
+
111
+ If everything passes, the script prints the exact `git tag` + `git push origin`
112
+ commands. You then push the tag and the workflow takes over.
113
+
114
+ If anything fails, fix the issue first. Do not push the tag.
115
+
116
+ ### Smoke-only workflow run
117
+
118
+ To validate the smoke script itself without publishing (e.g. after editing
119
+ `scripts/pre-publish-smoke.sh`):
120
+
121
+ ```
122
+ GitHub → Actions → Publish @floomhq/floom → Run workflow
123
+ smoke_only: true
124
+ ```
125
+
126
+ The workflow will build, run the smoke, and stop. No `npm publish`, no
127
+ Homebrew dispatch.
128
+
129
+ ### Manual publish (rare)
130
+
131
+ To publish without a tag (e.g. retag an existing version under a different
132
+ dist-tag), use `workflow_dispatch` with `smoke_only: false` and the desired
133
+ `tag` input. The smoke gate still runs first.
134
+
135
+ ## What the pre-publish smoke checks
136
+
137
+ See `scripts/pre-publish-smoke.sh` for the full assertion list. Summary:
138
+
139
+ | Stage | Asserts |
140
+ |------|---------|
141
+ | 1 | Tarball builds, packs, installs cleanly into isolated `$HOME`; installed `floom --version` matches `package.json` |
142
+ | 2 | **Every** subcommand `--help` exits 0, starts with `Usage: floom`, writes no `~/.floom/auth.json`, prints no "Signed (in|out)" / "Waiting for browser" (P0 #1 from 3.0.0) |
143
+ | 3 | `floom --help \| head -3` and `floom push --help \| head -1` print no EPIPE / "Unhandled error event" (P0 #2 from 3.0.0) |
144
+ | 4 | `floom <unknown>` exits non-zero with "unknown command" + "did you mean" hint (P0 #3 from 3.0.0) |
145
+ | 5 | Bare `floom --help` exits 0 with grouped help |
146
+ | 6 | Unauth `whoami` / `account` / `logout` exit 0 idempotent; unauth `status` / `sync --json` / `push --json` exit non-zero with no stack trace |
147
+ | 7 | `floom logout extra-positional` is rejected with non-zero exit AND `~/.floom/auth.json` is preserved (no destructive body) |
148
+ | 8 | **Agent detection** picks up fake `.claude/`, `.codex/`, `.cursor/` marker dirs in the isolated `$HOME` (verified by parsing `floom doctor --json`); bare `floom sync` / `floom pull` with planted agents do not crash unauth (clean `signedIn:false` envelope). Note: fan-out *behaviour itself* requires real auth, so it is exercised at the **unit-test layer** by `src/commands/sync.fanout.test.ts`, which the publish workflow runs as a separate hard gate before the smoke. |
149
+
150
+ If any assertion fails, the publish job fails. The smoke script exits non-zero
151
+ on the first failure and prints a summary of every failed check.
152
+
153
+ ## Files
154
+
155
+ ```
156
+ packages/cli/
157
+ ├── src/ # TypeScript source
158
+ │ ├── index.ts # CLI entry point (commander)
159
+ │ ├── commands/ # one file per subcommand
160
+ │ ├── lib/ # shared helpers
161
+ │ └── cli-smoke.test.ts # unit-level smoke (P0 lock-ins)
162
+ ├── scripts/
163
+ │ ├── build-bundle.mjs # esbuild → dist/index.js
164
+ │ ├── verify-package.mjs # pre-pack file allowlist check
165
+ │ └── pre-publish-smoke.sh # E2E smoke driven against tarball
166
+ ├── package.json
167
+ └── CHANGELOG.md
168
+ ```
package/dist/index.js CHANGED
@@ -3253,7 +3253,7 @@ async function getMachineIdentity() {
3253
3253
  }
3254
3254
 
3255
3255
  // src/version.ts
3256
- var VERSION = "3.0.1";
3256
+ var VERSION = "3.0.3";
3257
3257
 
3258
3258
  // src/api-client.ts
3259
3259
  var DEFAULT_TIMEOUT_MS = 2e4;
@@ -3721,17 +3721,35 @@ async function loginCommand() {
3721
3721
  }
3722
3722
 
3723
3723
  // src/commands/logout.ts
3724
- async function logoutCommand() {
3725
- const auth = await readAuth();
3724
+ async function logoutCommand(opts = {}, deps = {}) {
3725
+ const readAuth2 = deps.readAuth ?? readAuth;
3726
+ const clearAuth2 = deps.clearAuth ?? clearAuth;
3727
+ const api2 = deps.api ?? api;
3728
+ const auth = await readAuth2();
3729
+ let remoteRevokeFailed = false;
3726
3730
  if (auth) {
3727
3731
  try {
3728
- await api("/cli/session/revoke", { method: "POST", authRequired: true });
3732
+ await api2("/cli/session/revoke", {
3733
+ method: "POST",
3734
+ authRequired: true,
3735
+ query: opts.all ? { scope: "all" } : void 0
3736
+ });
3729
3737
  } catch (error) {
3730
- log.warn(`Remote session revoke failed: ${error.message}`);
3738
+ if (opts.all) {
3739
+ remoteRevokeFailed = true;
3740
+ process.exitCode = 1;
3741
+ log.err(
3742
+ "Local session cleared. Other devices are still authenticated.\nSign in at https://floom.dev/settings to revoke other sessions from the web."
3743
+ );
3744
+ } else {
3745
+ log.warn(`Remote session revoke failed: ${error.message}`);
3746
+ }
3731
3747
  }
3732
3748
  }
3733
- await clearAuth();
3734
- log.ok("Signed out of Floom on this machine.");
3749
+ await clearAuth2();
3750
+ log.ok(
3751
+ remoteRevokeFailed ? "Local sign-out only \u2014 remote revoke failed." : opts.all ? "Signed out of Floom CLI on all machines." : "Signed out of Floom on this machine."
3752
+ );
3735
3753
  log.blank();
3736
3754
  log.info("Your local skill files in ~/.claude/skills, ~/.codex/skills, etc. were not changed.");
3737
3755
  printNext([{ command: "floom login", description: "sign in again" }]);
@@ -5674,6 +5692,9 @@ async function pullCommand(skillArg, rawOpts = {}) {
5674
5692
  }
5675
5693
  }
5676
5694
 
5695
+ // src/commands/sync.ts
5696
+ import chalk5 from "chalk";
5697
+
5677
5698
  // src/commands/sync-runner.ts
5678
5699
  init_src();
5679
5700
  import { cp as cp2, mkdtemp, readdir as readdir4, rm as rm2, stat as stat6 } from "node:fs/promises";
@@ -5845,7 +5866,9 @@ async function runSyncForTarget(options = {}, deps = {}) {
5845
5866
  process.exitCode = 1;
5846
5867
  return { ok: false, pushFailures, hasConflicts: false };
5847
5868
  }
5848
- log.ok(`Sync complete. Pulled ${plan.pull.length} skills. Pushed ${pushed}/${plan.push.length} skills.`);
5869
+ if (!options.quietSuccess) {
5870
+ log.ok(`Sync complete. Pulled ${plan.pull.length} skills. Pushed ${pushed}/${plan.push.length} skills.`);
5871
+ }
5849
5872
  return { ok: true, pushFailures: [], hasConflicts: false };
5850
5873
  }
5851
5874
 
@@ -5877,9 +5900,12 @@ function buildApplyJson(workspaceName, applied, opts) {
5877
5900
  next: ["floom status"]
5878
5901
  };
5879
5902
  }
5880
- async function planForAgent(agent, scope, skillsDir) {
5903
+ function formatAgentLabel(plan) {
5904
+ return `${AGENT_LABELS[plan.agent]}${plan.scope === "project" ? " (project)" : ""}`;
5905
+ }
5906
+ async function planForAgent(agent, scope, skillsDir, statusFn = statusLibrary) {
5881
5907
  try {
5882
- const status = await statusLibrary(agent, { installDir: skillsDir });
5908
+ const status = await statusFn(agent, { installDir: skillsDir });
5883
5909
  return {
5884
5910
  agent,
5885
5911
  scope,
@@ -5892,7 +5918,12 @@ async function planForAgent(agent, scope, skillsDir) {
5892
5918
  return { agent, scope, skillsDir, pull: [], push: [], conflicts: [] };
5893
5919
  }
5894
5920
  }
5895
- async function syncCommand(rawOpts = {}) {
5921
+ async function syncCommand(rawOpts = {}, deps = {}) {
5922
+ const detectAgentsFn = deps.detectAgents ?? detectAgents;
5923
+ const runSyncFn = deps.runSyncForTarget ?? runSyncForTarget;
5924
+ const statusFn = deps.statusLibrary ?? statusLibrary;
5925
+ const fetchMeFn = deps.fetchMe ?? fetchMe;
5926
+ const readAuthFn = deps.readAuth ?? readAuth;
5896
5927
  const cleanup = installCancellationHandler();
5897
5928
  try {
5898
5929
  let flags;
@@ -5908,7 +5939,7 @@ async function syncCommand(rawOpts = {}) {
5908
5939
  }
5909
5940
  const json = flags.json;
5910
5941
  const planMode = isPlanMode(flags);
5911
- if (!await readAuth()) {
5942
+ if (!await readAuthFn()) {
5912
5943
  if (json) {
5913
5944
  emitJson({ workspace: { name: "Library", signedIn: false }, mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], next: ["floom login"] });
5914
5945
  } else {
@@ -5918,30 +5949,30 @@ async function syncCommand(rawOpts = {}) {
5918
5949
  process.exitCode = 2;
5919
5950
  return;
5920
5951
  }
5921
- const detected = await detectAgents();
5952
+ const detected = await detectAgentsFn();
5922
5953
  if (detected.length === 0) {
5923
5954
  if (json) {
5924
- emitJson({ workspace: { name: "Library", signedIn: true }, mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], next: ["floom sync --agent claude"] });
5955
+ emitJson({ workspace: { name: "Library", signedIn: true }, mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], error: "no agents detected \u2014 install Claude, Codex, Cursor, Gemini, or OpenCode", next: ["floom sync --agent claude"] });
5925
5956
  } else {
5926
- log.info("No AI agents found on this machine.");
5927
- log.info("Floom looks for Claude, Codex, Cursor, Gemini, or OpenCode.");
5957
+ log.err("No agents detected \u2014 install Claude, Codex, Cursor, Gemini, or OpenCode, then re-run.");
5958
+ log.info("Floom syncs into one of: Claude, Codex, Cursor, Gemini, OpenCode.");
5959
+ log.info("More: https://floom.dev/docs#agents");
5928
5960
  }
5929
5961
  process.exitCode = 1;
5930
5962
  return;
5931
5963
  }
5964
+ const defaultedToAll = flags.agents.length === 0 && !flags.allAgents;
5932
5965
  const selectedAgents = flags.agents.length > 0 ? flags.agents : detected;
5933
- if (!planMode && flags.agents.length === 0 && !flags.allAgents && !isInteractive()) {
5966
+ if (!planMode && !flags.yes && !isInteractive()) {
5934
5967
  if (json) {
5935
- emitJson({ workspace: { name: "Library", signedIn: true }, error: "floom sync needs to know which agents to update", next: ["floom sync --all-agents --yes"] });
5968
+ emitJson({ workspace: { name: "Library", signedIn: true }, error: "floom sync needs --yes to apply changes in a non-interactive shell", next: ["floom sync --yes"] });
5936
5969
  } else {
5937
- log.err("floom sync needs to know which agents to update.");
5938
- log.blank();
5939
- log.info("Pass --agent <name>, repeat --agent, or use --all-agents.");
5970
+ log.err("floom sync needs --yes to apply changes in a non-interactive shell.");
5940
5971
  log.blank();
5941
5972
  log.info("Examples:");
5973
+ log.command("floom sync --yes # sync every detected agent");
5942
5974
  log.command("floom sync --agent claude --yes");
5943
5975
  log.command("floom sync --agent claude,codex --yes");
5944
- log.command("floom sync --all-agents --yes");
5945
5976
  }
5946
5977
  process.exitCode = 2;
5947
5978
  return;
@@ -5951,7 +5982,7 @@ async function syncCommand(rawOpts = {}) {
5951
5982
  process.exitCode = 2;
5952
5983
  return;
5953
5984
  }
5954
- const me = await fetchMe().catch(() => null);
5985
+ const me = await fetchMeFn().catch(() => null);
5955
5986
  const workspaceName = me?.workspace?.name ?? "your Workspace";
5956
5987
  const hasProjectScope = await projectSkillsDirExists(process.cwd());
5957
5988
  const scopeChoice = flags.allScopes ? ["global", "project"] : flags.scope === "project" ? ["project"] : planMode && flags.scope === null && hasProjectScope ? ["global", "project"] : ["global"];
@@ -5960,7 +5991,7 @@ async function syncCommand(rawOpts = {}) {
5960
5991
  for (const scope of scopeChoice) {
5961
5992
  const dir = scope === "project" ? resolve6(projectSkillsDir(agent)) : resolve6(globalSkillsDir(agent));
5962
5993
  if (scope === "project" && !await projectSkillsDirExists(process.cwd())) continue;
5963
- plans.push(await planForAgent(agent, scope, dir));
5994
+ plans.push(await planForAgent(agent, scope, dir, statusFn));
5964
5995
  }
5965
5996
  }
5966
5997
  const actionable = plans.filter((p) => p.pull.length > 0 || p.push.length > 0);
@@ -6013,12 +6044,24 @@ async function syncCommand(rawOpts = {}) {
6013
6044
  return;
6014
6045
  }
6015
6046
  if (!wouldMutate && !hasSkipped) {
6047
+ if (json) {
6048
+ const noopApplied = plans.map((p) => ({ plan: p, ok: true, message: "up to date" }));
6049
+ emitJson(buildApplyJson(workspaceName, noopApplied, { failed: false, hasSkipped: false }));
6050
+ return;
6051
+ }
6052
+ if (defaultedToAll && detected.length > 1) {
6053
+ log.info(`Syncing every detected agent (${detected.map((a) => AGENT_LABELS[a]).join(", ")}).`);
6054
+ }
6016
6055
  log.info("Everything is in sync.");
6017
6056
  log.blank();
6018
6057
  for (const p of plans) log.row([AGENT_LABELS[p.agent], "up to date"], [10]);
6019
6058
  printNext([{ command: "floom status" }]);
6020
6059
  return;
6021
6060
  }
6061
+ if (defaultedToAll && detected.length > 1) {
6062
+ log.info(`Syncing every detected agent (${detected.map((a) => AGENT_LABELS[a]).join(", ")}).`);
6063
+ log.blank();
6064
+ }
6022
6065
  log.heading("Sync plan for this machine");
6023
6066
  log.blank();
6024
6067
  for (const p of plans) {
@@ -6056,7 +6099,7 @@ async function syncCommand(rawOpts = {}) {
6056
6099
  const applied = [];
6057
6100
  for (const p of toSync) {
6058
6101
  try {
6059
- const result = await runSyncForTarget({ target: p.agent, yes: true, installDir: p.skillsDir });
6102
+ const result = await runSyncFn({ target: p.agent, yes: true, installDir: p.skillsDir, quietSuccess: true });
6060
6103
  const entry = appliedFromResult(p, result);
6061
6104
  if (!entry.ok) failed = true;
6062
6105
  applied.push(entry);
@@ -6071,6 +6114,32 @@ async function syncCommand(rawOpts = {}) {
6071
6114
  if (failed || hasSkipped) process.exitCode = 1;
6072
6115
  return;
6073
6116
  }
6117
+ if (failed) {
6118
+ const failedCount = applied.filter((a) => !a.ok).length;
6119
+ const succeededCount = applied.filter((a) => a.ok).length;
6120
+ log.blank();
6121
+ log.info(`Sync completed with ${failedCount} target(s) failing (${succeededCount} succeeded).`);
6122
+ for (const entry of applied) {
6123
+ const marker = entry.ok ? chalk5.green("\u2713") : chalk5.red("\u2717");
6124
+ log.info(` ${marker} ${formatAgentLabel(entry.plan)} ${entry.message}`);
6125
+ }
6126
+ log.blank();
6127
+ printNext([{ command: "floom status" }]);
6128
+ if (hasSkipped) {
6129
+ for (const p of plans) {
6130
+ for (const slug of p.conflicts) {
6131
+ log.warn(`${AGENT_LABELS[p.agent]} skipped ${slug}, changed in two places`);
6132
+ }
6133
+ }
6134
+ }
6135
+ process.exitCode = 1;
6136
+ return;
6137
+ }
6138
+ if (!hasSkipped) {
6139
+ const pulled = applied.reduce((n, a) => n + a.plan.pull.length, 0);
6140
+ const pushed = applied.reduce((n, a) => n + a.plan.push.length, 0);
6141
+ log.ok(`Sync complete. Pulled ${pulled} skill${pulled === 1 ? "" : "s"}. Pushed ${pushed}/${pushed} skill${pushed === 1 ? "" : "s"}.`);
6142
+ }
6074
6143
  for (const p of plans) {
6075
6144
  for (const slug of p.conflicts) {
6076
6145
  log.warn(`${AGENT_LABELS[p.agent]} skipped ${slug}, changed in two places`);
@@ -6087,7 +6156,7 @@ async function syncCommand(rawOpts = {}) {
6087
6156
  log.info("Backups of any replaced skills are in each agent's .floom/backups/ folder (run floom restore --list to see them).");
6088
6157
  printNext([{ command: "floom status" }]);
6089
6158
  }
6090
- if (failed || hasSkipped) process.exitCode = 1;
6159
+ if (hasSkipped) process.exitCode = 1;
6091
6160
  } finally {
6092
6161
  cleanup.dispose();
6093
6162
  }
@@ -6095,20 +6164,20 @@ async function syncCommand(rawOpts = {}) {
6095
6164
 
6096
6165
  // src/commands/status.ts
6097
6166
  import { resolve as resolve7 } from "node:path";
6098
- import chalk5 from "chalk";
6167
+ import chalk6 from "chalk";
6099
6168
  init_runtime();
6100
6169
  var AGENT_TIMEOUT_MS = 1e4;
6101
6170
  var MAX_ATTENTION_ROWS = 15;
6102
6171
  function toFloomState(state) {
6103
6172
  switch (state) {
6104
6173
  case "active":
6105
- return { state: "up_to_date", label: "up to date", color: chalk5.green };
6174
+ return { state: "up_to_date", label: "up to date", color: chalk6.green };
6106
6175
  case "stale":
6107
- return { state: "update_available", label: "update available", color: chalk5.yellow };
6176
+ return { state: "update_available", label: "update available", color: chalk6.yellow };
6108
6177
  case "dirty":
6109
- return { state: "local_changes", label: "local changes", color: chalk5.yellow };
6178
+ return { state: "local_changes", label: "local changes", color: chalk6.yellow };
6110
6179
  case "conflict":
6111
- return { state: "changed_in_two_places", label: "changed in two places", color: chalk5.red };
6180
+ return { state: "changed_in_two_places", label: "changed in two places", color: chalk6.red };
6112
6181
  case "missing":
6113
6182
  return { state: "not_installed", label: "not installed", color: (s) => s };
6114
6183
  case "unsupported_target":
@@ -6291,7 +6360,7 @@ async function statusCommand(rawOpts = {}) {
6291
6360
  log.blank();
6292
6361
  const shown = attention.slice(0, MAX_ATTENTION_ROWS);
6293
6362
  for (const item of shown) {
6294
- log.info(` ${chalk5.bold(item.slug)}`);
6363
+ log.info(` ${chalk6.bold(item.slug)}`);
6295
6364
  for (const loc of item.locations) {
6296
6365
  const c = toFloomLabel(loc.state);
6297
6366
  log.info(` ${AGENT_LABELS[loc.agent].padEnd(10)} ${c.color(c.label.padEnd(22))}${tildePath(loc.path)}`);
@@ -6368,13 +6437,13 @@ function summarize(skills) {
6368
6437
  function toFloomLabel(state) {
6369
6438
  switch (state) {
6370
6439
  case "up_to_date":
6371
- return { label: "up to date", color: chalk5.green };
6440
+ return { label: "up to date", color: chalk6.green };
6372
6441
  case "local_changes":
6373
- return { label: "local changes", color: chalk5.yellow };
6442
+ return { label: "local changes", color: chalk6.yellow };
6374
6443
  case "update_available":
6375
- return { label: "update available", color: chalk5.yellow };
6444
+ return { label: "update available", color: chalk6.yellow };
6376
6445
  case "changed_in_two_places":
6377
- return { label: "changed in two places", color: chalk5.red };
6446
+ return { label: "changed in two places", color: chalk6.red };
6378
6447
  case "not_in_library_never_published":
6379
6448
  return { label: "not in Library", color: (s) => s };
6380
6449
  case "not_in_library_removed":
@@ -7890,7 +7959,7 @@ async function dashboardCommand() {
7890
7959
  }
7891
7960
 
7892
7961
  // src/lib/help.ts
7893
- import chalk6 from "chalk";
7962
+ import chalk7 from "chalk";
7894
7963
  var GROUPS = [
7895
7964
  {
7896
7965
  title: "Sign in",
@@ -7960,21 +8029,21 @@ var COMMON_FLAGS = `Common flags
7960
8029
  --no-secret-check with push, skip the pre-publish secret scan`;
7961
8030
  function printGroupedHelp() {
7962
8031
  const out2 = process.stdout;
7963
- out2.write("\n" + chalk6.bold("Usage: floom [command]") + "\n\n");
7964
- out2.write(` ${chalk6.cyan.bold("floom")} your dashboard \u2014 Library and agent status at a glance
8032
+ out2.write("\n" + chalk7.bold("Usage: floom [command]") + "\n\n");
8033
+ out2.write(` ${chalk7.cyan.bold("floom")} your dashboard \u2014 Library and agent status at a glance
7965
8034
  `);
7966
8035
  const allNames = GROUPS.flatMap((g) => g.rows.map((r) => r.name));
7967
8036
  const width = Math.max(...allNames.map((n) => n.length), 14);
7968
8037
  for (const group of GROUPS) {
7969
- out2.write("\n" + chalk6.bold(group.title) + "\n");
8038
+ out2.write("\n" + chalk7.bold(group.title) + "\n");
7970
8039
  for (const row of group.rows) {
7971
- const aliasNote = row.alias ? chalk6.dim(` ${row.alias} (alias)`) : "";
7972
- out2.write(` ${chalk6.cyan.bold(row.name.padEnd(width + 2))}${row.description}${aliasNote}
8040
+ const aliasNote = row.alias ? chalk7.dim(` ${row.alias} (alias)`) : "";
8041
+ out2.write(` ${chalk7.cyan.bold(row.name.padEnd(width + 2))}${row.description}${aliasNote}
7973
8042
  `);
7974
8043
  }
7975
8044
  }
7976
8045
  out2.write("\n" + COMMON_FLAGS + "\n");
7977
- out2.write("\n" + chalk6.dim("More help: https://floom.dev/docs") + "\n");
8046
+ out2.write("\n" + chalk7.dim("More help: https://floom.dev/docs") + "\n");
7978
8047
  }
7979
8048
 
7980
8049
  // src/index.ts
@@ -8037,7 +8106,7 @@ function editDistance(a, b) {
8037
8106
  return curr[b.length];
8038
8107
  }
8039
8108
  helpOpt(program.command("login").description("sign in to your Floom workspace")).action(loginCommand);
8040
- helpOpt(program.command("logout").description("sign out on this machine")).action(logoutCommand);
8109
+ helpOpt(program.command("logout").description("sign out on this machine").option("--all", "sign out all CLI sessions for this account")).action((opts) => logoutCommand(opts));
8041
8110
  helpOpt(program.command("account").description("show account details").option("--json", "print account state as JSON")).action((opts) => accountCommand(opts));
8042
8111
  helpOpt(program.command("whoami").description("show account details (alias)").option("--json", "print account state as JSON")).action((opts) => {
8043
8112
  aliasNotice("whoami", "account");
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "3.0.1";
1
+ export const VERSION = "3.0.3";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floomhq/floom",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
4
4
  "description": "Floom CLI \u2014 one shared skill library, pulled into the AI agent you choose (Claude, Codex, Cursor, Gemini, OpenCode).",
5
5
  "license": "MIT",
6
6
  "homepage": "https://floom.dev",