@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 +168 -0
- package/dist/index.js +113 -44
- package/dist/version.js +1 -1
- package/package.json +1 -1
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
3734
|
-
log.ok(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
5927
|
-
log.info("Floom
|
|
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 &&
|
|
5966
|
+
if (!planMode && !flags.yes && !isInteractive()) {
|
|
5934
5967
|
if (json) {
|
|
5935
|
-
emitJson({ workspace: { name: "Library", signedIn: true }, error: "floom sync needs to
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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:
|
|
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:
|
|
6176
|
+
return { state: "update_available", label: "update available", color: chalk6.yellow };
|
|
6108
6177
|
case "dirty":
|
|
6109
|
-
return { state: "local_changes", label: "local changes", color:
|
|
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:
|
|
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(` ${
|
|
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:
|
|
6440
|
+
return { label: "up to date", color: chalk6.green };
|
|
6372
6441
|
case "local_changes":
|
|
6373
|
-
return { label: "local changes", color:
|
|
6442
|
+
return { label: "local changes", color: chalk6.yellow };
|
|
6374
6443
|
case "update_available":
|
|
6375
|
-
return { label: "update available", color:
|
|
6444
|
+
return { label: "update available", color: chalk6.yellow };
|
|
6376
6445
|
case "changed_in_two_places":
|
|
6377
|
-
return { label: "changed in two places", color:
|
|
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
|
|
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" +
|
|
7964
|
-
out2.write(` ${
|
|
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" +
|
|
8038
|
+
out2.write("\n" + chalk7.bold(group.title) + "\n");
|
|
7970
8039
|
for (const row of group.rows) {
|
|
7971
|
-
const aliasNote = row.alias ?
|
|
7972
|
-
out2.write(` ${
|
|
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" +
|
|
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
|
+
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.
|
|
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",
|