@floomhq/floom 3.0.2 → 3.0.4

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/dist/index.js CHANGED
@@ -3067,6 +3067,33 @@ init_src();
3067
3067
  import { homedir as homedir2 } from "node:os";
3068
3068
  import { join as join3 } from "node:path";
3069
3069
  import { mkdir as mkdir2, readFile as readFile3, writeFile, chmod } from "node:fs/promises";
3070
+
3071
+ // src/lib/invocation.ts
3072
+ var NPX_HINTS = ["_npx", "npm-cache/_npx", "npm-cache\\_npx", ".npm/_npx"];
3073
+ var NPX_INVOCATION = "npx -y @floomhq/floom";
3074
+ var GLOBAL_INVOCATION = "floom";
3075
+ var cached = null;
3076
+ function detect() {
3077
+ const override = process.env.FLOOM_INVOKED_AS;
3078
+ if (override && (override === NPX_INVOCATION || override === GLOBAL_INVOCATION)) {
3079
+ return override;
3080
+ }
3081
+ const argv1 = process.argv[1] ?? "";
3082
+ for (const hint of NPX_HINTS) {
3083
+ if (argv1.includes(hint)) return NPX_INVOCATION;
3084
+ }
3085
+ return GLOBAL_INVOCATION;
3086
+ }
3087
+ function cliInvocation() {
3088
+ if (cached === null) cached = detect();
3089
+ return cached;
3090
+ }
3091
+ function cliCmd(cmd) {
3092
+ const stripped = cmd.startsWith("floom ") ? cmd.slice("floom ".length) : cmd;
3093
+ return `${cliInvocation()} ${stripped}`;
3094
+ }
3095
+
3096
+ // src/config.ts
3070
3097
  var CONFIG_DIR = join3(homedir2(), ".floom");
3071
3098
  var AUTH_FILE = join3(CONFIG_DIR, "auth.json");
3072
3099
  var DEFAULT_APP_URL = "https://floom.dev";
@@ -3119,18 +3146,25 @@ async function readRawAuth() {
3119
3146
  if (code === "ENOENT") return null;
3120
3147
  if (code === "EACCES" || code === "EPERM") {
3121
3148
  throw new Error(
3122
- "Cannot read your Floom auth file (permission denied).\n Fix: chmod 600 ~/.floom/auth.json\n Or re-authenticate: npx -y @floomhq/floom login\n More help: https://floom.dev/docs#troubleshooting"
3149
+ `Cannot read your Floom auth file (permission denied).
3150
+ Fix: chmod 600 ~/.floom/auth.json
3151
+ Or re-authenticate: ${cliCmd("login")}
3152
+ More help: https://floom.dev/docs#troubleshooting`
3123
3153
  );
3124
3154
  }
3125
3155
  throw new Error(
3126
- "Cannot read your Floom auth file.\n Run: npx -y @floomhq/floom login\n More help: https://floom.dev/docs#troubleshooting"
3156
+ `Cannot read your Floom auth file.
3157
+ Run: ${cliCmd("login")}
3158
+ More help: https://floom.dev/docs#troubleshooting`
3127
3159
  );
3128
3160
  }
3129
3161
  try {
3130
3162
  return JSON.parse(raw);
3131
3163
  } catch {
3132
3164
  throw new Error(
3133
- "Your Floom auth file is corrupted.\n Run: npx -y @floomhq/floom login to generate a fresh one.\n More help: https://floom.dev/docs#troubleshooting"
3165
+ `Your Floom auth file is corrupted.
3166
+ Run: ${cliCmd("login")} to generate a fresh one.
3167
+ More help: https://floom.dev/docs#troubleshooting`
3134
3168
  );
3135
3169
  }
3136
3170
  }
@@ -3253,7 +3287,7 @@ async function getMachineIdentity() {
3253
3287
  }
3254
3288
 
3255
3289
  // src/version.ts
3256
- var VERSION = "3.0.2";
3290
+ var VERSION = "3.0.4";
3257
3291
 
3258
3292
  // src/api-client.ts
3259
3293
  var DEFAULT_TIMEOUT_MS = 2e4;
@@ -3304,12 +3338,12 @@ var HELP = "https://floom.dev/docs#troubleshooting";
3304
3338
  function friendlyApiErrorMessage(code, raw, status) {
3305
3339
  if (code === "AUTH_REQUIRED" || code === "TOKEN_INVALID" || status === 401) {
3306
3340
  return `Not signed in.
3307
- Run: floom login
3341
+ Run: ${cliCmd("login")}
3308
3342
  More help: ${HELP}`;
3309
3343
  }
3310
3344
  if (code === "TOKEN_EXPIRED") {
3311
3345
  return `Your session has expired.
3312
- Run: floom login
3346
+ Run: ${cliCmd("login")}
3313
3347
  More help: ${HELP}`;
3314
3348
  }
3315
3349
  if (code === "SKILL_ACCESS_DENIED" || status === 403) {
@@ -3318,7 +3352,7 @@ function friendlyApiErrorMessage(code, raw, status) {
3318
3352
  }
3319
3353
  if (code === "SKILL_NOT_FOUND" || status === 404) {
3320
3354
  return `Skill not found. It may have been deleted or the slug is wrong.
3321
- Run: floom list
3355
+ Run: ${cliCmd("list")}
3322
3356
  More help: ${HELP}`;
3323
3357
  }
3324
3358
  if (code === "RATE_LIMITED" || status === 429) {
@@ -3346,7 +3380,9 @@ async function api(path, opts = {}) {
3346
3380
  if (opts.authRequired && !token) {
3347
3381
  throw new FloomError(
3348
3382
  "AUTH_REQUIRED",
3349
- "Not signed in.\n Run: floom login\n More help: https://floom.dev/docs#troubleshooting"
3383
+ `Not signed in.
3384
+ Run: ${cliCmd("login")}
3385
+ More help: https://floom.dev/docs#troubleshooting`
3350
3386
  );
3351
3387
  }
3352
3388
  let lastError = null;
@@ -3425,7 +3461,7 @@ async function api(path, opts = {}) {
3425
3461
  // src/lib/agents.ts
3426
3462
  init_src();
3427
3463
  import { createHash as createHash3 } from "node:crypto";
3428
- import { readdir as readdir2, readFile as readFile5, stat as stat3, lstat as lstat2 } from "node:fs/promises";
3464
+ import { readdir as readdir2, readFile as readFile5, readlink, stat as stat3, lstat as lstat2 } from "node:fs/promises";
3429
3465
  import { homedir as homedir4 } from "node:os";
3430
3466
  import { join as join5, posix as posix2, relative as relative3, resolve as resolve2, sep as sep3 } from "node:path";
3431
3467
  var AGENT_LABELS = {
@@ -3550,7 +3586,28 @@ async function collectHashFiles(root) {
3550
3586
  const abs = join5(dir, entry.name);
3551
3587
  const info = await lstat2(abs);
3552
3588
  if (info.isSymbolicLink()) {
3553
- throw new Error(`skill folders must not contain symlinks: ${abs}`);
3589
+ let target = "(unreadable)";
3590
+ try {
3591
+ target = await readlink(abs);
3592
+ } catch {
3593
+ }
3594
+ throw new Error(
3595
+ `Skill folder contains a symlink (not allowed for security):
3596
+ link: ${abs}
3597
+ target: ${target}
3598
+
3599
+ Why: a symlink could point outside the skill folder and quietly leak files
3600
+ (SSH keys, .env, cookie jars) into a public Library. Floom refuses to read
3601
+ through symlinks for that reason.
3602
+
3603
+ Fix: replace the link with the real file contents:
3604
+ rm "${abs}"
3605
+ cp "${target}" "${abs}"
3606
+
3607
+ (or move the target file under the skill folder if it lives elsewhere)
3608
+
3609
+ More help: https://floom.dev/docs#skill-folder-rules`
3610
+ );
3554
3611
  }
3555
3612
  if (entry.isDirectory()) {
3556
3613
  await walk2(abs, childRel);
@@ -3622,8 +3679,16 @@ function tildePath(path, homeDir = homedir4()) {
3622
3679
  }
3623
3680
 
3624
3681
  // src/commands/login.ts
3682
+ function isHeadlessLinuxSession(env = process.env, platform3 = process.platform) {
3683
+ if (env.FLOOM_NO_OPEN === "1") return true;
3684
+ if (platform3 !== "linux") return false;
3685
+ const isSsh = Boolean(env.SSH_CONNECTION ?? env.SSH_CLIENT ?? env.SSH_TTY);
3686
+ const hasDisplay = Boolean(env.DISPLAY ?? env.WAYLAND_DISPLAY);
3687
+ return isSsh || !hasDisplay;
3688
+ }
3625
3689
  function tryOpenBrowser(url) {
3626
3690
  if (process.env.FLOOM_NO_OPEN === "1") return;
3691
+ if (isHeadlessLinuxSession()) return;
3627
3692
  const platform3 = process.platform;
3628
3693
  let cmd;
3629
3694
  let args;
@@ -3666,8 +3731,17 @@ async function loginCommand() {
3666
3731
  });
3667
3732
  log.heading("Sign in to Floom");
3668
3733
  log.blank();
3669
- log.info(` Open: ${session.verification_uri}`);
3670
- log.info(` Code: ${chalk2.bold.cyan(session.user_code)}`);
3734
+ const headless = isHeadlessLinuxSession();
3735
+ if (headless) {
3736
+ log.info(" Open this URL in any browser to approve:");
3737
+ log.info(` ${session.verification_uri}`);
3738
+ log.info(` Code: ${chalk2.bold.cyan(session.user_code)}`);
3739
+ log.blank();
3740
+ log.info(" You can open it on your phone or laptop \u2014 the same URL works from anywhere.");
3741
+ } else {
3742
+ log.info(` Open: ${session.verification_uri}`);
3743
+ log.info(` Code: ${chalk2.bold.cyan(session.user_code)}`);
3744
+ }
3671
3745
  log.blank();
3672
3746
  process.stdout.write("Waiting for browser approval...");
3673
3747
  tryOpenBrowser(session.verification_uri);
@@ -3693,9 +3767,13 @@ async function loginCommand() {
3693
3767
  if (token.workspace?.name) log.info(` Workspace: ${token.workspace.name}`);
3694
3768
  await printAgentsFound();
3695
3769
  printNext([
3696
- { command: "floom pull", description: "install your team's skills into these agents" },
3697
- { command: "floom push", description: "if you have skills here to publish first" }
3770
+ { command: cliCmd("pull"), description: "install your team's skills into these agents" },
3771
+ { command: cliCmd("push"), description: "if you have skills here to publish first" }
3698
3772
  ]);
3773
+ if (cliCmd("login").startsWith("npx ")) {
3774
+ log.blank();
3775
+ log.info(" Tip: install once with npm i -g @floomhq/floom and just type `floom` after that.");
3776
+ }
3699
3777
  return;
3700
3778
  }
3701
3779
  } catch (error) {
@@ -3714,27 +3792,45 @@ async function loginCommand() {
3714
3792
  process.stdout.write("\n");
3715
3793
  log.err("Login timed out \u2014 the browser window expired.");
3716
3794
  printNext([
3717
- { command: "floom login", description: "try again" },
3795
+ { command: cliCmd("login"), description: "try again" },
3718
3796
  { command: "More help: https://floom.dev/docs#troubleshooting" }
3719
3797
  ]);
3720
3798
  process.exitCode = 1;
3721
3799
  }
3722
3800
 
3723
3801
  // src/commands/logout.ts
3724
- async function logoutCommand() {
3725
- const auth = await readAuth();
3802
+ async function logoutCommand(opts = {}, deps = {}) {
3803
+ const readAuth2 = deps.readAuth ?? readAuth;
3804
+ const clearAuth2 = deps.clearAuth ?? clearAuth;
3805
+ const api2 = deps.api ?? api;
3806
+ const auth = await readAuth2();
3807
+ let remoteRevokeFailed = false;
3726
3808
  if (auth) {
3727
3809
  try {
3728
- await api("/cli/session/revoke", { method: "POST", authRequired: true });
3810
+ await api2("/cli/session/revoke", {
3811
+ method: "POST",
3812
+ authRequired: true,
3813
+ query: opts.all ? { scope: "all" } : void 0
3814
+ });
3729
3815
  } catch (error) {
3730
- log.warn(`Remote session revoke failed: ${error.message}`);
3816
+ if (opts.all) {
3817
+ remoteRevokeFailed = true;
3818
+ process.exitCode = 1;
3819
+ log.err(
3820
+ "Local session cleared. Other devices are still authenticated.\nSign in at https://floom.dev/settings to revoke other sessions from the web."
3821
+ );
3822
+ } else {
3823
+ log.warn(`Remote session revoke failed: ${error.message}`);
3824
+ }
3731
3825
  }
3732
3826
  }
3733
- await clearAuth();
3734
- log.ok("Signed out of Floom on this machine.");
3827
+ await clearAuth2();
3828
+ log.ok(
3829
+ 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."
3830
+ );
3735
3831
  log.blank();
3736
3832
  log.info("Your local skill files in ~/.claude/skills, ~/.codex/skills, etc. were not changed.");
3737
- printNext([{ command: "floom login", description: "sign in again" }]);
3833
+ printNext([{ command: cliCmd("login"), description: "sign in again" }]);
3738
3834
  }
3739
3835
 
3740
3836
  // src/commands/account.ts
@@ -3789,18 +3885,18 @@ async function accountCommand(opts = {}) {
3789
3885
  const device = await deviceLabel();
3790
3886
  if (!auth) {
3791
3887
  if (json) {
3792
- emitJson({ signedIn: false, email: null, workspace: null, device: device ? { label: device } : null, next: ["floom login"] });
3888
+ emitJson({ signedIn: false, email: null, workspace: null, device: device ? { label: device } : null, next: [cliCmd("login")] });
3793
3889
  return;
3794
3890
  }
3795
3891
  log.info("Not signed in to Floom.");
3796
- printNext([{ command: "floom login" }]);
3892
+ printNext([{ command: cliCmd("login") }]);
3797
3893
  return;
3798
3894
  }
3799
3895
  if (!json) {
3800
3896
  const rawAuth = await readRawAuth();
3801
3897
  if (rawAuth && !isTrustedApiUrl(rawAuth.apiUrl)) {
3802
3898
  log.warn(
3803
- `Your saved Floom credentials point at ${rawAuth.apiUrl}, which is not a trusted Floom API. Run 'floom login' to re-authenticate.`
3899
+ `Your saved Floom credentials point at ${rawAuth.apiUrl}, which is not a trusted Floom API. Run '${cliCmd("login")}' to re-authenticate.`
3804
3900
  );
3805
3901
  }
3806
3902
  }
@@ -3812,7 +3908,7 @@ async function accountCommand(opts = {}) {
3812
3908
  email: me.user.email ?? auth.email,
3813
3909
  workspace: me.workspace ? { name: me.workspace.name } : null,
3814
3910
  device: { label: device },
3815
- next: ["floom status", "floom logout"]
3911
+ next: [cliCmd("status"), cliCmd("logout")]
3816
3912
  });
3817
3913
  return;
3818
3914
  }
@@ -3822,19 +3918,19 @@ async function accountCommand(opts = {}) {
3822
3918
  if (me.workspace) log.kv("Workspace:", me.workspace.name);
3823
3919
  log.kv("This machine:", device ?? "not named yet");
3824
3920
  printNext([
3825
- { command: "floom status", description: "see Library and local agent copies" },
3826
- { command: "floom logout", description: "sign out on this machine" }
3921
+ { command: cliCmd("status"), description: "see Library and local agent copies" },
3922
+ { command: cliCmd("logout"), description: "sign out on this machine" }
3827
3923
  ]);
3828
3924
  } catch (error) {
3829
3925
  if (json) {
3830
- emitJson({ signedIn: false, email: auth.email, workspace: null, device: { label: device }, next: ["floom login"] });
3926
+ emitJson({ signedIn: false, email: auth.email, workspace: null, device: { label: device }, next: [cliCmd("login")] });
3831
3927
  process.exitCode = 1;
3832
3928
  return;
3833
3929
  }
3834
3930
  log.err(error.message);
3835
3931
  log.blank();
3836
3932
  log.info("Session expired. Sign in again to continue.");
3837
- printNext([{ command: "floom login" }]);
3933
+ printNext([{ command: cliCmd("login") }]);
3838
3934
  process.exitCode = 1;
3839
3935
  }
3840
3936
  void isJsonMode;
@@ -4702,7 +4798,7 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4702
4798
  syncedCount: 0,
4703
4799
  plan: [],
4704
4800
  error: { code: "AUTH_REQUIRED", message: "Not signed in \u2014 log in first to publish." },
4705
- next: ["floom login"]
4801
+ next: [cliCmd("login")]
4706
4802
  });
4707
4803
  process.exitCode = 1;
4708
4804
  return;
@@ -4712,7 +4808,7 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4712
4808
  log.blank();
4713
4809
  if (skills.length === 0) {
4714
4810
  log.info("No local skill folders found on this machine.");
4715
- printNext([{ command: "floom login" }]);
4811
+ printNext([{ command: cliCmd("login") }]);
4716
4812
  process.exitCode = 1;
4717
4813
  return;
4718
4814
  }
@@ -4720,10 +4816,10 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4720
4816
  for (const skill of skills) log.info(` ${tildePath(skill.path)}`);
4721
4817
  log.blank();
4722
4818
  log.info("To compare and publish them, sign in to Floom.");
4723
- printNext([{ command: "floom login" }]);
4819
+ printNext([{ command: cliCmd("login") }]);
4724
4820
  log.blank();
4725
4821
  log.info("After login, run:");
4726
- log.command("floom push");
4822
+ log.command(cliCmd("push"));
4727
4823
  process.exitCode = 1;
4728
4824
  return;
4729
4825
  }
@@ -4741,7 +4837,7 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4741
4837
  wouldMutate: false,
4742
4838
  syncedCount: 0,
4743
4839
  plan: [],
4744
- next: libCount > 0 ? ["floom pull"] : ["floom new"]
4840
+ next: libCount > 0 ? [cliCmd("pull")] : [cliCmd("new")]
4745
4841
  });
4746
4842
  return;
4747
4843
  }
@@ -4750,11 +4846,11 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4750
4846
  log.blank();
4751
4847
  if (libCount > 0) {
4752
4848
  log.info(`Your Library has ${libCount} skill${libCount === 1 ? "" : "s"}. To install them here, run:`);
4753
- log.command("floom pull");
4849
+ log.command(cliCmd("pull"));
4754
4850
  }
4755
4851
  printNext([
4756
- { command: "floom pull", description: "install your Library on this machine" },
4757
- { command: "floom new research-helper", description: "create a ready-to-edit skill" }
4852
+ { command: cliCmd("pull"), description: "install your Library on this machine" },
4853
+ { command: cliCmd("new research-helper"), description: "create a ready-to-edit skill" }
4758
4854
  ]);
4759
4855
  return;
4760
4856
  }
@@ -4797,7 +4893,7 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4797
4893
  lastSyncedLibraryVersion: r.lastSyncedLibraryVersion,
4798
4894
  publishVersion: r.publishVersion
4799
4895
  })),
4800
- next: ["floom push --yes", "floom push"]
4896
+ next: [cliCmd("push --yes"), cliCmd("push")]
4801
4897
  });
4802
4898
  process.exitCode = wouldMutate && !options.exitZero ? 1 : 0;
4803
4899
  return;
@@ -4819,7 +4915,7 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4819
4915
  if (!wouldMutate && refusedRows.length === 0) {
4820
4916
  log.blank();
4821
4917
  log.info("Everything is already in sync with the Library. Nothing to publish.");
4822
- printNext([{ command: "floom status", description: "see every copy" }]);
4918
+ printNext([{ command: cliCmd("status"), description: "see every copy" }]);
4823
4919
  return;
4824
4920
  }
4825
4921
  let toPublish = [];
@@ -4827,12 +4923,12 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4827
4923
  toPublish = safeRows;
4828
4924
  } else if (!isInteractive()) {
4829
4925
  log.blank();
4830
- log.err("floom push needs a choice but is not running in an interactive terminal.");
4926
+ log.err(`${cliCmd("push")} needs a choice but is not running in an interactive terminal.`);
4831
4927
  log.blank();
4832
4928
  log.info("Re-run with:");
4833
- log.command("floom push --dry-run preview what would happen");
4834
- log.command("floom push --yes proceed with all safe changes");
4835
- log.command("floom push --json get the plan as JSON");
4929
+ log.command(cliCmd("push --dry-run") + " preview what would happen");
4930
+ log.command(cliCmd("push --yes") + " proceed with all safe changes");
4931
+ log.command(cliCmd("push --json") + " get the plan as JSON");
4836
4932
  process.exitCode = 2;
4837
4933
  return;
4838
4934
  } else {
@@ -4882,9 +4978,9 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4882
4978
  }
4883
4979
  const conflictSlug = skipped.find((r) => r.action === "conflict")?.slug;
4884
4980
  printNext([
4885
- ...conflictSlug ? [{ command: `floom diff ${conflictSlug}`, description: "review the conflict before publishing" }] : [],
4886
- { command: "floom pull", description: "update your other agents with these new versions" },
4887
- { command: "floom status", description: "see every copy" }
4981
+ ...conflictSlug ? [{ command: cliCmd(`diff ${conflictSlug}`), description: "review the conflict before publishing" }] : [],
4982
+ { command: cliCmd("pull"), description: "update your other agents with these new versions" },
4983
+ { command: cliCmd("status"), description: "see every copy" }
4888
4984
  ]);
4889
4985
  if (failures.length > 0 || skipped.length > 0) process.exitCode = 1;
4890
4986
  } finally {
@@ -4897,10 +4993,10 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
4897
4993
  const info = await stat5(folder).catch(() => null);
4898
4994
  if (!info || !info.isDirectory()) {
4899
4995
  if (json) {
4900
- emitJson({ workspace: { name: "Library", signedIn: authed }, mode: "plan", applied: false, wouldMutate: false, syncedCount: 0, plan: [], next: ["floom push"] });
4996
+ emitJson({ workspace: { name: "Library", signedIn: authed }, mode: "plan", applied: false, wouldMutate: false, syncedCount: 0, plan: [], next: [cliCmd("push")] });
4901
4997
  } else {
4902
4998
  log.err(`Folder not found: ${pathArg}`);
4903
- log.info(" Run floom push with no arguments to auto-detect your skills, or pass a valid folder path.");
4999
+ log.info(` Run ${cliCmd("push")} with no arguments to auto-detect your skills, or pass a valid folder path.`);
4904
5000
  }
4905
5001
  process.exitCode = 2;
4906
5002
  return;
@@ -4923,13 +5019,13 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
4923
5019
  syncedCount: 0,
4924
5020
  plan: [],
4925
5021
  error: { code: "AUTH_REQUIRED", message: "Not signed in \u2014 log in first to publish." },
4926
- next: ["floom login"]
5022
+ next: [cliCmd("login")]
4927
5023
  });
4928
5024
  } else {
4929
5025
  log.blank();
4930
5026
  log.err("Not signed in \u2014 log in first to publish.");
4931
5027
  log.info(`Found local skill at ${tildePath(folder)}.`);
4932
- printNext([{ command: "floom login" }]);
5028
+ printNext([{ command: cliCmd("login") }]);
4933
5029
  }
4934
5030
  process.exitCode = 1;
4935
5031
  return;
@@ -4946,7 +5042,7 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
4946
5042
  wouldMutate: false,
4947
5043
  syncedCount: 0,
4948
5044
  plan: [{ slug, scope: "global", path: folder, action: "changed", status: "refused", refusedReason: "secret_scan_failed", message: `${slug} was refused because the secret scan could not run`, localHash: "", lastSyncedHash: null, libraryVersion: null, lastSyncedLibraryVersion: null, publishVersion: null }],
4949
- next: ["floom push --no-secret-check"]
5045
+ next: [cliCmd("push --no-secret-check")]
4950
5046
  });
4951
5047
  process.exitCode = 1;
4952
5048
  return;
@@ -4968,7 +5064,7 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
4968
5064
  wouldMutate: false,
4969
5065
  syncedCount: 0,
4970
5066
  plan: [{ slug, scope: "global", path: folder, action: "changed", status: "refused", refusedReason: "secret_detected", message: `${slug} was refused because a file looks like it contains a secret`, localHash: "", lastSyncedHash: null, libraryVersion: null, lastSyncedLibraryVersion: null, publishVersion: null }],
4971
- next: ["floom push --no-secret-check"]
5067
+ next: [cliCmd("push --no-secret-check")]
4972
5068
  });
4973
5069
  process.exitCode = 1;
4974
5070
  return;
@@ -4993,7 +5089,7 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
4993
5089
  log.info("Nothing published.");
4994
5090
  printNext([
4995
5091
  { command: "Edit .floomignore to exclude secrets or generated files" },
4996
- { command: `floom push ${pathArg}` }
5092
+ { command: cliCmd(`push ${pathArg}`) }
4997
5093
  ]);
4998
5094
  return;
4999
5095
  }
@@ -5008,7 +5104,7 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
5008
5104
  wouldMutate: true,
5009
5105
  syncedCount: 0,
5010
5106
  plan: [{ slug, scope: "global", path: folder, action: "changed", status: "ok", refusedReason: null, message: `${slug} would be published`, localHash: await skillContentHash(folder) ?? "", lastSyncedHash: null, libraryVersion: null, lastSyncedLibraryVersion: null, publishVersion: null }],
5011
- next: ["floom push " + pathArg + " --yes"]
5107
+ next: [cliCmd(`push ${pathArg} --yes`)]
5012
5108
  });
5013
5109
  } else {
5014
5110
  log.heading("Push plan");
@@ -5028,15 +5124,15 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
5028
5124
  mode: "apply",
5029
5125
  applied: true,
5030
5126
  results: [{ slug: result.skill.slug, scope: "global", path: folder, action: "changed", result: "published", message: `${result.skill.slug} published as ${result.skill.latest.display}`, libraryVersion: result.skill.latest.version_seq ?? null, publishVersion: result.skill.latest.version_seq ?? null, backupPath: null }],
5031
- next: ["floom pull"]
5127
+ next: [cliCmd("pull")]
5032
5128
  });
5033
5129
  } else {
5034
5130
  log.ok(`Published ${result.skill.slug} ${result.skill.latest.display}`);
5035
- printNext([{ command: "floom pull", description: `update your other agents with ${result.skill.slug}` }]);
5131
+ printNext([{ command: cliCmd("pull"), description: `update your other agents with ${result.skill.slug}` }]);
5036
5132
  }
5037
5133
  } catch (error) {
5038
5134
  if (json) {
5039
- emitJson({ workspace: { name: "Library", signedIn: true }, mode: "apply", applied: true, results: [{ slug, scope: "global", path: folder, action: "changed", result: "failed", message: error.message, libraryVersion: null, publishVersion: null, backupPath: null }], next: ["floom push " + pathArg] });
5135
+ emitJson({ workspace: { name: "Library", signedIn: true }, mode: "apply", applied: true, results: [{ slug, scope: "global", path: folder, action: "changed", result: "failed", message: error.message, libraryVersion: null, publishVersion: null, backupPath: null }], next: [cliCmd(`push ${pathArg}`)] });
5040
5136
  } else {
5041
5137
  log.err(error.message);
5042
5138
  }
@@ -5077,11 +5173,11 @@ async function deleteCommand(slug, opts = {}, deps = { api }) {
5077
5173
  skill: { slug: "", version: null, exists: false },
5078
5174
  wouldMutate: false,
5079
5175
  action: "missing_argument",
5080
- message: "floom delete needs a skill name.",
5081
- next: ["floom list"]
5176
+ message: `${cliCmd("delete")} needs a skill name.`,
5177
+ next: [cliCmd("list")]
5082
5178
  });
5083
5179
  } else {
5084
- log.err("floom delete needs a skill name. Run `floom list` to see skills in the Library.");
5180
+ log.err(`${cliCmd("delete")} needs a skill name. Run \`${cliCmd("list")}\` to see skills in the Library.`);
5085
5181
  }
5086
5182
  process.exitCode = 2;
5087
5183
  return;
@@ -5098,10 +5194,10 @@ async function deleteCommand(slug, opts = {}, deps = { api }) {
5098
5194
  wouldMutate: false,
5099
5195
  action: "not_found",
5100
5196
  message: `No skill named '${slug}' in ${ws}.`,
5101
- next: ["floom list"]
5197
+ next: [cliCmd("list")]
5102
5198
  });
5103
5199
  } else {
5104
- log.err(`No skill named '${slug}' in ${ws}. Run \`floom list\` to see what is in the Library.`);
5200
+ log.err(`No skill named '${slug}' in ${ws}. Run \`${cliCmd("list")}\` to see what is in the Library.`);
5105
5201
  }
5106
5202
  process.exitCode = 2;
5107
5203
  return;
@@ -5116,7 +5212,7 @@ async function deleteCommand(slug, opts = {}, deps = { api }) {
5116
5212
  wouldMutate: true,
5117
5213
  action: "delete",
5118
5214
  message: `${slug} would be deleted from the Library.`,
5119
- next: ["floom delete " + slug + " --yes"]
5215
+ next: [cliCmd(`delete ${slug} --yes`)]
5120
5216
  });
5121
5217
  } else {
5122
5218
  log.heading(`Delete plan for ${ws} Library`);
@@ -5142,17 +5238,17 @@ async function deleteCommand(slug, opts = {}, deps = { api }) {
5142
5238
  wouldMutate: true,
5143
5239
  action: "delete",
5144
5240
  message: `${slug} would be deleted from the Library.`,
5145
- next: ["floom delete " + slug + " --yes"]
5241
+ next: [cliCmd(`delete ${slug} --yes`)]
5146
5242
  });
5147
5243
  process.exitCode = opts.exitZero ? 0 : 1;
5148
5244
  return;
5149
5245
  }
5150
5246
  if (!opts.yes) {
5151
5247
  if (!isInteractive()) {
5152
- log.err("floom delete needs confirmation but is not running in an interactive terminal.");
5248
+ log.err(`${cliCmd("delete")} needs confirmation but is not running in an interactive terminal.`);
5153
5249
  log.blank();
5154
5250
  log.info("Re-run with:");
5155
- log.command(`floom delete ${slug} --yes`);
5251
+ log.command(cliCmd(`delete ${slug} --yes`));
5156
5252
  process.exitCode = 2;
5157
5253
  return;
5158
5254
  }
@@ -5181,7 +5277,7 @@ async function deleteCommand(slug, opts = {}, deps = { api }) {
5181
5277
  skill: { slug, version: info.versionSeq, exists: true },
5182
5278
  result: "failed",
5183
5279
  message: error.message,
5184
- next: ["floom delete " + slug + " --yes"]
5280
+ next: [cliCmd(`delete ${slug} --yes`)]
5185
5281
  });
5186
5282
  } else {
5187
5283
  log.err(error.message);
@@ -5197,14 +5293,14 @@ async function deleteCommand(slug, opts = {}, deps = { api }) {
5197
5293
  skill: { slug, version: info.versionSeq, exists: false },
5198
5294
  result: "deleted",
5199
5295
  message: `Deleted ${slug} from the Library.`,
5200
- next: ["floom status"]
5296
+ next: [cliCmd("status")]
5201
5297
  });
5202
5298
  return;
5203
5299
  }
5204
5300
  log.ok(`Deleted ${slug} from the Library.`);
5205
5301
  log.blank();
5206
5302
  log.info("Local copies in your agents still have the skill. Floom will not delete them automatically.");
5207
- printNext([{ command: "floom status", description: "see which local copies are no longer in the Library" }]);
5303
+ printNext([{ command: cliCmd("status"), description: "see which local copies are no longer in the Library" }]);
5208
5304
  }
5209
5305
 
5210
5306
  // src/commands/list.ts
@@ -5218,11 +5314,11 @@ async function listCommand(opts = {}) {
5218
5314
  workspace: { name: "Library", signedIn: false },
5219
5315
  error: { code: "AUTH_REQUIRED", message: "Not signed in." },
5220
5316
  skills: [],
5221
- next: ["floom login"]
5317
+ next: [cliCmd("login")]
5222
5318
  });
5223
5319
  } else {
5224
5320
  log.err("Not signed in.");
5225
- printNext([{ command: "floom login" }]);
5321
+ printNext([{ command: cliCmd("login") }]);
5226
5322
  }
5227
5323
  process.exitCode = 1;
5228
5324
  return;
@@ -5237,7 +5333,7 @@ async function listCommand(opts = {}) {
5237
5333
  workspace: { name: "Library", signedIn: true },
5238
5334
  error: { code: fe?.code ?? "INTERNAL_ERROR", message: e.message },
5239
5335
  skills: [],
5240
- next: ["floom login"]
5336
+ next: [cliCmd("login")]
5241
5337
  });
5242
5338
  process.exitCode = 1;
5243
5339
  return;
@@ -5271,7 +5367,7 @@ async function listCommand(opts = {}) {
5271
5367
  }))
5272
5368
  };
5273
5369
  }),
5274
- next: result.total === 0 ? ["floom push"] : ["floom pull", "floom status"]
5370
+ next: result.total === 0 ? [cliCmd("push")] : [cliCmd("pull"), cliCmd("status")]
5275
5371
  });
5276
5372
  return;
5277
5373
  }
@@ -5286,7 +5382,7 @@ async function listCommand(opts = {}) {
5286
5382
  log.info(` ${tildePath(skill.path)}`);
5287
5383
  }
5288
5384
  }
5289
- printNext([{ command: "floom push", description: "publish your first skills to the Library" }]);
5385
+ printNext([{ command: cliCmd("push"), description: "publish your first skills to the Library" }]);
5290
5386
  return;
5291
5387
  }
5292
5388
  log.heading(`Library \xB7 ${result.total} skill${result.total === 1 ? "" : "s"}`);
@@ -5304,8 +5400,8 @@ async function listCommand(opts = {}) {
5304
5400
  log.info(` ... and ${result.total - rows.length} more`);
5305
5401
  }
5306
5402
  printNext([
5307
- { command: "floom pull", description: "install missing skills into local agents" },
5308
- { command: "floom status", description: "compare every local copy in detail" }
5403
+ { command: cliCmd("pull"), description: "install missing skills into local agents" },
5404
+ { command: cliCmd("status"), description: "compare every local copy in detail" }
5309
5405
  ]);
5310
5406
  }
5311
5407
 
@@ -5370,10 +5466,10 @@ async function pullCommand(skillArg, rawOpts = {}) {
5370
5466
  const planMode = isPlanMode(flags);
5371
5467
  if (!await readAuth()) {
5372
5468
  if (json) {
5373
- emitJson({ workspace: { name: "Library", signedIn: false }, mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], next: ["floom login"] });
5469
+ emitJson({ workspace: { name: "Library", signedIn: false }, mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], next: [cliCmd("login")] });
5374
5470
  } else {
5375
5471
  log.info("Not signed in to Floom.");
5376
- printNext([{ command: "floom login" }]);
5472
+ printNext([{ command: cliCmd("login") }]);
5377
5473
  }
5378
5474
  process.exitCode = 2;
5379
5475
  return;
@@ -5381,14 +5477,14 @@ async function pullCommand(skillArg, rawOpts = {}) {
5381
5477
  const detected = await detectAgents();
5382
5478
  if (detected.length === 0) {
5383
5479
  if (json) {
5384
- emitJson({ workspace: { name: "Library", signedIn: true }, mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], next: ["floom pull --agent claude"] });
5480
+ emitJson({ workspace: { name: "Library", signedIn: true }, mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], next: [cliCmd("pull --agent claude")] });
5385
5481
  } else {
5386
5482
  log.info("No AI agents found on this machine.");
5387
5483
  log.blank();
5388
5484
  log.info("Floom looks for Claude, Codex, Cursor, Gemini, or OpenCode.");
5389
- log.info("Make sure one of these is installed, then run floom pull again.");
5485
+ log.info(`Make sure one of these is installed, then run ${cliCmd("pull")} again.`);
5390
5486
  log.blank();
5391
- log.info("Or use: floom pull --agent claude");
5487
+ log.info(`Or use: ${cliCmd("pull --agent claude")}`);
5392
5488
  log.blank();
5393
5489
  log.info("More help: https://floom.dev/docs#agents");
5394
5490
  }
@@ -5400,19 +5496,19 @@ async function pullCommand(skillArg, rawOpts = {}) {
5400
5496
  if (skillArg) {
5401
5497
  if (flags.allScopes) {
5402
5498
  if (json) {
5403
- emitJson({ workspace: { name: "Library", signedIn: true }, error: "floom pull <skill> targets one copy \u2014 pass --global or --project, not --all-scopes.", next: [`floom pull ${skillArg} --agent claude --global`] });
5499
+ emitJson({ workspace: { name: "Library", signedIn: true }, error: `${cliCmd("pull")} <skill> targets one copy \u2014 pass --global or --project, not --all-scopes.`, next: [cliCmd(`pull ${skillArg} --agent claude --global`)] });
5404
5500
  } else {
5405
- log.err("floom pull <skill> targets one copy \u2014 pass --global or --project, not --all-scopes.");
5501
+ log.err(`${cliCmd("pull")} <skill> targets one copy \u2014 pass --global or --project, not --all-scopes.`);
5406
5502
  }
5407
5503
  process.exitCode = 2;
5408
5504
  return;
5409
5505
  }
5410
5506
  if (flags.agents.length !== 1) {
5411
5507
  if (json) {
5412
- emitJson({ workspace: { name: "Library", signedIn: true }, error: "floom pull <skill> needs exactly one --agent", next: [`floom pull ${skillArg} --agent claude`] });
5508
+ emitJson({ workspace: { name: "Library", signedIn: true }, error: `${cliCmd("pull")} <skill> needs exactly one --agent`, next: [cliCmd(`pull ${skillArg} --agent claude`)] });
5413
5509
  } else {
5414
- log.err(`floom pull ${skillArg} needs exactly one --agent <name>.`);
5415
- log.command(`floom pull ${skillArg} --agent claude`);
5510
+ log.err(`${cliCmd("pull")} ${skillArg} needs exactly one --agent <name>.`);
5511
+ log.command(cliCmd(`pull ${skillArg} --agent claude`));
5416
5512
  }
5417
5513
  process.exitCode = 2;
5418
5514
  return;
@@ -5421,7 +5517,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
5421
5517
  const hasProjectSkillsDir = await projectSkillsDirExists(process.cwd());
5422
5518
  if (flags.scope === "project" && !hasProjectSkillsDir) {
5423
5519
  if (json) {
5424
- emitJson({ workspace: { name: "Library", signedIn: true }, error: "No project agent skill directory found. Pass --global or run from a project with agent skills.", next: [`floom pull ${skillArg} --agent ${agent} --global`] });
5520
+ emitJson({ workspace: { name: "Library", signedIn: true }, error: "No project agent skill directory found. Pass --global or run from a project with agent skills.", next: [cliCmd(`pull ${skillArg} --agent ${agent} --global`)] });
5425
5521
  } else {
5426
5522
  log.err("No project agent skill directory found. Pass --global or run from a project with agent skills.");
5427
5523
  }
@@ -5430,7 +5526,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
5430
5526
  }
5431
5527
  if (flags.scope === null && hasProjectSkillsDir && !isInteractive()) {
5432
5528
  if (json) {
5433
- emitJson({ workspace: { name: "Library", signedIn: true }, error: "This project has its own agent skill copies. Pass --global or --project.", next: [`floom pull ${skillArg} --agent ${agent} --global`, `floom pull ${skillArg} --agent ${agent} --project`] });
5529
+ emitJson({ workspace: { name: "Library", signedIn: true }, error: "This project has its own agent skill copies. Pass --global or --project.", next: [cliCmd(`pull ${skillArg} --agent ${agent} --global`), cliCmd(`pull ${skillArg} --agent ${agent} --project`)] });
5434
5530
  } else {
5435
5531
  log.err("This project has its own agent skill copies. Pass --global or --project.");
5436
5532
  }
@@ -5439,7 +5535,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
5439
5535
  }
5440
5536
  const resolvedScope = flags.scope === "project" ? "project" : "global";
5441
5537
  const dir = resolve5(resolvedScope === "project" ? projectSkillsDir(agent) : globalSkillsDir(agent));
5442
- const scopedPullCommand = `floom pull ${skillArg} --agent ${agent}${resolvedScope === "project" ? " --project" : ""}`;
5538
+ const scopedPullCommand = cliCmd(`pull ${skillArg} --agent ${agent}${resolvedScope === "project" ? " --project" : ""}`);
5443
5539
  if (flags.dryRun || json && !flags.yes) {
5444
5540
  if (json) {
5445
5541
  emitJson({ workspace: { name: "Library", signedIn: true }, mode: "plan", applied: false, wouldMutate: true, hasSkipped: false, agents: [{ name: agent, scope: resolvedScope, skillsDir: dir, actions: [{ slug: skillArg, scope: resolvedScope, action: "update", fromVersion: null, toVersion: null, path: join9(dir, skillArg), backupPath: null }] }], next: [`${scopedPullCommand} --yes`] });
@@ -5455,10 +5551,10 @@ async function pullCommand(skillArg, rawOpts = {}) {
5455
5551
  cleanup.trackDir(join9(dir, ".floom", "tmp"));
5456
5552
  const result = await pullOneSkill(agent, skillArg, { installDir: dir });
5457
5553
  if (json) {
5458
- emitJson({ workspace: { name: "Library", signedIn: true }, mode: "apply", applied: true, hasFailures: false, agents: [{ name: agent, scope: resolvedScope, skillsDir: dir, results: [{ slug: result.slug, scope: resolvedScope, action: "update", result: "updated", fromVersion: null, toVersion: result.versionSeq, path: join9(dir, result.slug), backupPath: null, message: `updated to v${result.versionSeq}` }] }], next: [`floom status --agent ${agent}`] });
5554
+ emitJson({ workspace: { name: "Library", signedIn: true }, mode: "apply", applied: true, hasFailures: false, agents: [{ name: agent, scope: resolvedScope, skillsDir: dir, results: [{ slug: result.slug, scope: resolvedScope, action: "update", result: "updated", fromVersion: null, toVersion: result.versionSeq, path: join9(dir, result.slug), backupPath: null, message: `updated to v${result.versionSeq}` }] }], next: [cliCmd(`status --agent ${agent}`)] });
5459
5555
  } else {
5460
5556
  log.ok(`Backed up your local copy and updated ${result.slug} to Library v${result.versionSeq}`);
5461
- printNext([{ command: `floom status --agent ${agent}` }]);
5557
+ printNext([{ command: cliCmd(`status --agent ${agent}`) }]);
5462
5558
  }
5463
5559
  } catch (error) {
5464
5560
  if (json) {
@@ -5472,16 +5568,16 @@ async function pullCommand(skillArg, rawOpts = {}) {
5472
5568
  }
5473
5569
  if (!planMode && flags.agents.length === 0 && !wantAll && !isInteractive()) {
5474
5570
  if (json) {
5475
- emitJson({ workspace: { name: "Library", signedIn: true }, error: "floom pull needs to know which agents to update", next: ["floom pull --all-agents --yes"] });
5571
+ emitJson({ workspace: { name: "Library", signedIn: true }, error: `${cliCmd("pull")} needs to know which agents to update`, next: [cliCmd("pull --all-agents --yes")] });
5476
5572
  } else {
5477
- log.err("floom pull needs to know which agents to update.");
5573
+ log.err(`${cliCmd("pull")} needs to know which agents to update.`);
5478
5574
  log.blank();
5479
5575
  log.info("Pass --agent <name>, repeat --agent, or use --all-agents.");
5480
5576
  log.blank();
5481
5577
  log.info("Examples:");
5482
- log.command("floom pull --agent claude --yes");
5483
- log.command("floom pull --agent claude,codex --yes");
5484
- log.command("floom pull --all-agents --yes");
5578
+ log.command(cliCmd("pull --agent claude --yes"));
5579
+ log.command(cliCmd("pull --agent claude,codex --yes"));
5580
+ log.command(cliCmd("pull --all-agents --yes"));
5485
5581
  }
5486
5582
  process.exitCode = 2;
5487
5583
  return;
@@ -5526,7 +5622,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
5526
5622
  backupPath: null
5527
5623
  }))
5528
5624
  })),
5529
- next: ["floom pull --all-agents --yes"]
5625
+ next: [cliCmd("pull --all-agents --yes")]
5530
5626
  });
5531
5627
  process.exitCode = (wouldMutate || hasSkipped) && !flags.exitZero ? 1 : 0;
5532
5628
  return;
@@ -5564,7 +5660,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
5564
5660
  }
5565
5661
  log.blank();
5566
5662
  const firstConflict = conflicted[0];
5567
- printNext(firstConflict ? [{ command: `floom diff ${firstConflict.slug}`, description: "review the conflict" }] : [{ command: "floom status" }]);
5663
+ printNext(firstConflict ? [{ command: cliCmd(`diff ${firstConflict.slug}`), description: "review the conflict" }] : [{ command: cliCmd("status") }]);
5568
5664
  process.exitCode = flags.exitZero ? 0 : 1;
5569
5665
  return;
5570
5666
  }
@@ -5573,7 +5669,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
5573
5669
  for (const p of plans) {
5574
5670
  log.row([AGENT_LABELS[p.agent], "up to date"], [10]);
5575
5671
  }
5576
- printNext([{ command: "floom status" }]);
5672
+ printNext([{ command: cliCmd("status") }]);
5577
5673
  return;
5578
5674
  }
5579
5675
  log.heading(`Update agent copies from ${workspaceName}`);
@@ -5590,7 +5686,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
5590
5686
  let toUpdate = actionable;
5591
5687
  if (!flags.yes) {
5592
5688
  if (!isInteractive()) {
5593
- log.err("floom pull needs a choice but is not running in an interactive terminal.");
5689
+ log.err(`${cliCmd("pull")} needs a choice but is not running in an interactive terminal.`);
5594
5690
  log.info("Re-run with --agent <name> --yes, or --all-agents --yes.");
5595
5691
  process.exitCode = 2;
5596
5692
  return;
@@ -5650,7 +5746,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
5650
5746
  message: a.message
5651
5747
  }]
5652
5748
  })),
5653
- next: ["floom status"]
5749
+ next: [cliCmd("status")]
5654
5750
  });
5655
5751
  if (failed || hasSkipped) process.exitCode = flags.exitZero ? 0 : 1;
5656
5752
  return;
@@ -5663,8 +5759,8 @@ async function pullCommand(skillArg, rawOpts = {}) {
5663
5759
  }
5664
5760
  }
5665
5761
  log.blank();
5666
- log.info("Backups of any replaced skills are in each agent's .floom/backups/ folder (run floom restore --list to see them).");
5667
- printNext([{ command: "floom status" }]);
5762
+ log.info(`Backups of any replaced skills are in each agent's .floom/backups/ folder (run ${cliCmd("restore --list")} to see them).`);
5763
+ printNext([{ command: cliCmd("status") }]);
5668
5764
  if (failed || hasSkipped) process.exitCode = flags.exitZero ? 0 : 1;
5669
5765
  void skillArg;
5670
5766
  void INSTALL_TARGETS;
@@ -5674,6 +5770,9 @@ async function pullCommand(skillArg, rawOpts = {}) {
5674
5770
  }
5675
5771
  }
5676
5772
 
5773
+ // src/commands/sync.ts
5774
+ import chalk5 from "chalk";
5775
+
5677
5776
  // src/commands/sync-runner.ts
5678
5777
  init_src();
5679
5778
  import { cp as cp2, mkdtemp, readdir as readdir4, rm as rm2, stat as stat6 } from "node:fs/promises";
@@ -5714,8 +5813,8 @@ function printPreview(plan) {
5714
5813
  log.err(`${count} ${noun} both locally and on the server: Floom won't guess which wins.`);
5715
5814
  for (const skill of plan.conflicts) {
5716
5815
  log.err(` - ${skill.slug} (${skill.version})`);
5717
- log.err(` Keep the server version: floom pull --target ${plan.target}`);
5718
- log.err(` Keep your local version: floom push <${skill.slug}-dir>`);
5816
+ log.err(` Keep the server version: ${cliCmd(`pull --target ${plan.target}`)}`);
5817
+ log.err(` Keep your local version: ${cliCmd(`push <${skill.slug}-dir>`)}`);
5719
5818
  }
5720
5819
  log.err("Your local copy is always backed up to .floom/backups/ first.");
5721
5820
  log.err("More: https://floom.dev/docs#conflicts");
@@ -5841,11 +5940,13 @@ async function runSyncForTarget(options = {}, deps = {}) {
5841
5940
  log.err(` snapshot at: ${failure.snapshotDir}`);
5842
5941
  }
5843
5942
  log.err("");
5844
- log.err(`Re-run \`floom sync --target ${plan.target}\` after the network recovers.`);
5943
+ log.err(`Re-run \`${cliCmd(`sync --target ${plan.target}`)}\` after the network recovers.`);
5845
5944
  process.exitCode = 1;
5846
5945
  return { ok: false, pushFailures, hasConflicts: false };
5847
5946
  }
5848
- log.ok(`Sync complete. Pulled ${plan.pull.length} skills. Pushed ${pushed}/${plan.push.length} skills.`);
5947
+ if (!options.quietSuccess) {
5948
+ log.ok(`Sync complete. Pulled ${plan.pull.length} skills. Pushed ${pushed}/${plan.push.length} skills.`);
5949
+ }
5849
5950
  return { ok: true, pushFailures: [], hasConflicts: false };
5850
5951
  }
5851
5952
 
@@ -5853,6 +5954,13 @@ async function runSyncForTarget(options = {}, deps = {}) {
5853
5954
  init_runtime();
5854
5955
  init_prompt();
5855
5956
  import { resolve as resolve6 } from "node:path";
5957
+ async function runTestSyncDelay(cleanup) {
5958
+ const delayMs = Number.parseInt(process.env.FLOOM_TEST_SYNC_DELAY_MS ?? "", 10);
5959
+ if (!Number.isFinite(delayMs) || delayMs <= 0) return;
5960
+ const trackDir = process.env.FLOOM_TEST_SYNC_TRACK_DIR;
5961
+ if (trackDir) cleanup.trackDir(trackDir);
5962
+ await new Promise((resolve14) => setTimeout(resolve14, delayMs));
5963
+ }
5856
5964
  function appliedFromResult(plan, result) {
5857
5965
  if (result.ok) return { plan, ok: true, message: "synced" };
5858
5966
  const message = result.pushFailures.length > 0 ? `push failed for ${result.pushFailures.map((f) => f.slug).join(", ")}` : result.hasConflicts ? "changed in two places" : "sync did not complete";
@@ -5874,9 +5982,12 @@ function buildApplyJson(workspaceName, applied, opts) {
5874
5982
  ...a.plan.conflicts.map((slug) => ({ slug, scope: a.plan.scope, direction: "pull", result: "skipped", fromVersion: null, toVersion: null, path: a.plan.skillsDir, backupPath: null, message: "changed in two places" }))
5875
5983
  ]
5876
5984
  })),
5877
- next: ["floom status"]
5985
+ next: [cliCmd("status")]
5878
5986
  };
5879
5987
  }
5988
+ function formatAgentLabel(plan) {
5989
+ return `${AGENT_LABELS[plan.agent]}${plan.scope === "project" ? " (project)" : ""}`;
5990
+ }
5880
5991
  async function planForAgent(agent, scope, skillsDir, statusFn = statusLibrary) {
5881
5992
  try {
5882
5993
  const status = await statusFn(agent, { installDir: skillsDir });
@@ -5900,6 +6011,7 @@ async function syncCommand(rawOpts = {}, deps = {}) {
5900
6011
  const readAuthFn = deps.readAuth ?? readAuth;
5901
6012
  const cleanup = installCancellationHandler();
5902
6013
  try {
6014
+ await runTestSyncDelay(cleanup);
5903
6015
  let flags;
5904
6016
  try {
5905
6017
  flags = parseCommonFlags(rawOpts);
@@ -5915,10 +6027,10 @@ async function syncCommand(rawOpts = {}, deps = {}) {
5915
6027
  const planMode = isPlanMode(flags);
5916
6028
  if (!await readAuthFn()) {
5917
6029
  if (json) {
5918
- emitJson({ workspace: { name: "Library", signedIn: false }, mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], next: ["floom login"] });
6030
+ emitJson({ workspace: { name: "Library", signedIn: false }, mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], next: [cliCmd("login")] });
5919
6031
  } else {
5920
6032
  log.info("Not signed in to Floom.");
5921
- printNext([{ command: "floom login" }]);
6033
+ printNext([{ command: cliCmd("login") }]);
5922
6034
  }
5923
6035
  process.exitCode = 2;
5924
6036
  return;
@@ -5926,7 +6038,7 @@ async function syncCommand(rawOpts = {}, deps = {}) {
5926
6038
  const detected = await detectAgentsFn();
5927
6039
  if (detected.length === 0) {
5928
6040
  if (json) {
5929
- 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"] });
6041
+ 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: [cliCmd("sync --agent claude")] });
5930
6042
  } else {
5931
6043
  log.err("No agents detected \u2014 install Claude, Codex, Cursor, Gemini, or OpenCode, then re-run.");
5932
6044
  log.info("Floom syncs into one of: Claude, Codex, Cursor, Gemini, OpenCode.");
@@ -5939,14 +6051,14 @@ async function syncCommand(rawOpts = {}, deps = {}) {
5939
6051
  const selectedAgents = flags.agents.length > 0 ? flags.agents : detected;
5940
6052
  if (!planMode && !flags.yes && !isInteractive()) {
5941
6053
  if (json) {
5942
- emitJson({ workspace: { name: "Library", signedIn: true }, error: "floom sync needs --yes to apply changes in a non-interactive shell", next: ["floom sync --yes"] });
6054
+ emitJson({ workspace: { name: "Library", signedIn: true }, error: `${cliCmd("sync")} needs --yes to apply changes in a non-interactive shell`, next: [cliCmd("sync --yes")] });
5943
6055
  } else {
5944
- log.err("floom sync needs --yes to apply changes in a non-interactive shell.");
6056
+ log.err(`${cliCmd("sync")} needs --yes to apply changes in a non-interactive shell.`);
5945
6057
  log.blank();
5946
6058
  log.info("Examples:");
5947
- log.command("floom sync --yes # sync every detected agent");
5948
- log.command("floom sync --agent claude --yes");
5949
- log.command("floom sync --agent claude,codex --yes");
6059
+ log.command(cliCmd("sync --yes") + " # sync every detected agent");
6060
+ log.command(cliCmd("sync --agent claude --yes"));
6061
+ log.command(cliCmd("sync --agent claude,codex --yes"));
5950
6062
  }
5951
6063
  process.exitCode = 2;
5952
6064
  return;
@@ -5990,12 +6102,12 @@ async function syncCommand(rawOpts = {}, deps = {}) {
5990
6102
  reason: "changed_in_two_places",
5991
6103
  libraryVersion: 0,
5992
6104
  localPath: p.skillsDir,
5993
- diffCommand: `floom diff ${slug}`,
5994
- pullCommand: `floom pull ${slug} --agent ${p.agent}${p.scope === "project" ? " --project" : ""}`,
5995
- pushCommand: `floom push`
6105
+ diffCommand: cliCmd(`diff ${slug}`),
6106
+ pullCommand: cliCmd(`pull ${slug} --agent ${p.agent}${p.scope === "project" ? " --project" : ""}`),
6107
+ pushCommand: cliCmd("push")
5996
6108
  }))
5997
6109
  })),
5998
- next: ["floom sync --all-agents --yes"]
6110
+ next: [cliCmd("sync --all-agents --yes")]
5999
6111
  });
6000
6112
  process.exitCode = (wouldMutate || hasSkipped) && !flags.exitZero ? 1 : 0;
6001
6113
  return;
@@ -6029,7 +6141,7 @@ async function syncCommand(rawOpts = {}, deps = {}) {
6029
6141
  log.info("Everything is in sync.");
6030
6142
  log.blank();
6031
6143
  for (const p of plans) log.row([AGENT_LABELS[p.agent], "up to date"], [10]);
6032
- printNext([{ command: "floom status" }]);
6144
+ printNext([{ command: cliCmd("status") }]);
6033
6145
  return;
6034
6146
  }
6035
6147
  if (defaultedToAll && detected.length > 1) {
@@ -6048,7 +6160,7 @@ async function syncCommand(rawOpts = {}, deps = {}) {
6048
6160
  let toSync = actionable;
6049
6161
  if (!flags.yes) {
6050
6162
  if (!isInteractive()) {
6051
- log.err("floom sync needs a choice but is not running in an interactive terminal.");
6163
+ log.err(`${cliCmd("sync")} needs a choice but is not running in an interactive terminal.`);
6052
6164
  log.info("Re-run with --agent <name> --yes, or --all-agents --yes.");
6053
6165
  process.exitCode = 2;
6054
6166
  return;
@@ -6073,7 +6185,7 @@ async function syncCommand(rawOpts = {}, deps = {}) {
6073
6185
  const applied = [];
6074
6186
  for (const p of toSync) {
6075
6187
  try {
6076
- const result = await runSyncFn({ target: p.agent, yes: true, installDir: p.skillsDir });
6188
+ const result = await runSyncFn({ target: p.agent, yes: true, installDir: p.skillsDir, quietSuccess: true });
6077
6189
  const entry = appliedFromResult(p, result);
6078
6190
  if (!entry.ok) failed = true;
6079
6191
  applied.push(entry);
@@ -6088,6 +6200,32 @@ async function syncCommand(rawOpts = {}, deps = {}) {
6088
6200
  if (failed || hasSkipped) process.exitCode = 1;
6089
6201
  return;
6090
6202
  }
6203
+ if (failed) {
6204
+ const failedCount = applied.filter((a) => !a.ok).length;
6205
+ const succeededCount = applied.filter((a) => a.ok).length;
6206
+ log.blank();
6207
+ log.info(`Sync completed with ${failedCount} target(s) failing (${succeededCount} succeeded).`);
6208
+ for (const entry of applied) {
6209
+ const marker = entry.ok ? chalk5.green("\u2713") : chalk5.red("\u2717");
6210
+ log.info(` ${marker} ${formatAgentLabel(entry.plan)} ${entry.message}`);
6211
+ }
6212
+ log.blank();
6213
+ printNext([{ command: cliCmd("status") }]);
6214
+ if (hasSkipped) {
6215
+ for (const p of plans) {
6216
+ for (const slug of p.conflicts) {
6217
+ log.warn(`${AGENT_LABELS[p.agent]} skipped ${slug}, changed in two places`);
6218
+ }
6219
+ }
6220
+ }
6221
+ process.exitCode = 1;
6222
+ return;
6223
+ }
6224
+ if (!hasSkipped) {
6225
+ const pulled = applied.reduce((n, a) => n + a.plan.pull.length, 0);
6226
+ const pushed = applied.reduce((n, a) => n + a.plan.push.length, 0);
6227
+ log.ok(`Sync complete. Pulled ${pulled} skill${pulled === 1 ? "" : "s"}. Pushed ${pushed}/${pushed} skill${pushed === 1 ? "" : "s"}.`);
6228
+ }
6091
6229
  for (const p of plans) {
6092
6230
  for (const slug of p.conflicts) {
6093
6231
  log.warn(`${AGENT_LABELS[p.agent]} skipped ${slug}, changed in two places`);
@@ -6096,15 +6234,15 @@ async function syncCommand(rawOpts = {}, deps = {}) {
6096
6234
  if (hasSkipped) {
6097
6235
  const conflictSlug = plans.flatMap((p) => p.conflicts)[0];
6098
6236
  printNext([
6099
- ...conflictSlug ? [{ command: `floom diff ${conflictSlug}`, description: "show what changed before choosing" }] : [],
6100
- { command: "floom status" }
6237
+ ...conflictSlug ? [{ command: cliCmd(`diff ${conflictSlug}`), description: "show what changed before choosing" }] : [],
6238
+ { command: cliCmd("status") }
6101
6239
  ]);
6102
6240
  } else {
6103
6241
  log.blank();
6104
- log.info("Backups of any replaced skills are in each agent's .floom/backups/ folder (run floom restore --list to see them).");
6105
- printNext([{ command: "floom status" }]);
6242
+ log.info(`Backups of any replaced skills are in each agent's .floom/backups/ folder (run ${cliCmd("restore --list")} to see them).`);
6243
+ printNext([{ command: cliCmd("status") }]);
6106
6244
  }
6107
- if (failed || hasSkipped) process.exitCode = 1;
6245
+ if (hasSkipped) process.exitCode = 1;
6108
6246
  } finally {
6109
6247
  cleanup.dispose();
6110
6248
  }
@@ -6112,20 +6250,20 @@ async function syncCommand(rawOpts = {}, deps = {}) {
6112
6250
 
6113
6251
  // src/commands/status.ts
6114
6252
  import { resolve as resolve7 } from "node:path";
6115
- import chalk5 from "chalk";
6253
+ import chalk6 from "chalk";
6116
6254
  init_runtime();
6117
6255
  var AGENT_TIMEOUT_MS = 1e4;
6118
6256
  var MAX_ATTENTION_ROWS = 15;
6119
6257
  function toFloomState(state) {
6120
6258
  switch (state) {
6121
6259
  case "active":
6122
- return { state: "up_to_date", label: "up to date", color: chalk5.green };
6260
+ return { state: "up_to_date", label: "up to date", color: chalk6.green };
6123
6261
  case "stale":
6124
- return { state: "update_available", label: "update available", color: chalk5.yellow };
6262
+ return { state: "update_available", label: "update available", color: chalk6.yellow };
6125
6263
  case "dirty":
6126
- return { state: "local_changes", label: "local changes", color: chalk5.yellow };
6264
+ return { state: "local_changes", label: "local changes", color: chalk6.yellow };
6127
6265
  case "conflict":
6128
- return { state: "changed_in_two_places", label: "changed in two places", color: chalk5.red };
6266
+ return { state: "changed_in_two_places", label: "changed in two places", color: chalk6.red };
6129
6267
  case "missing":
6130
6268
  return { state: "not_installed", label: "not installed", color: (s) => s };
6131
6269
  case "unsupported_target":
@@ -6181,15 +6319,15 @@ async function statusCommand(rawOpts = {}) {
6181
6319
  const detected = await detectAgents();
6182
6320
  if (detected.length === 0) {
6183
6321
  if (json) {
6184
- emitJson({ workspace: { name: "Library", signedIn: false, librarySkillCount: null }, complete: true, agents: [], skillsNeedingAttention: [], next: ["floom install <link>"] });
6322
+ emitJson({ workspace: { name: "Library", signedIn: false, librarySkillCount: null }, complete: true, agents: [], skillsNeedingAttention: [], next: [cliCmd("install <link>")] });
6185
6323
  } else {
6186
6324
  log.info("No AI agents found on this machine.");
6187
6325
  log.blank();
6188
6326
  log.info("Floom looks for Claude, Codex, Cursor, Gemini, or OpenCode.");
6189
- log.info("Install one of those agents, then run floom status again.");
6327
+ log.info(`Install one of those agents, then run ${cliCmd("status")} again.`);
6190
6328
  log.blank();
6191
6329
  log.info("If someone sent you a shared skill:");
6192
- log.command("floom install <link>");
6330
+ log.command(cliCmd("install <link>"));
6193
6331
  log.blank();
6194
6332
  log.info("More help: https://floom.dev/docs#agents");
6195
6333
  }
@@ -6205,7 +6343,7 @@ async function statusCommand(rawOpts = {}) {
6205
6343
  complete: false,
6206
6344
  agents: detected.map((agent) => ({ name: agent, detected: true, state: null, error: null, summary: emptySummary(), copies: [] })),
6207
6345
  skillsNeedingAttention: [],
6208
- next: ["floom login"]
6346
+ next: [cliCmd("login")]
6209
6347
  });
6210
6348
  process.exitCode = 1;
6211
6349
  return;
@@ -6219,7 +6357,7 @@ async function statusCommand(rawOpts = {}) {
6219
6357
  log.blank();
6220
6358
  log.info("Floom Library");
6221
6359
  log.info(" Not signed in \u2014 can't compare with your team Library.");
6222
- printNext([{ command: "floom login", description: "see and sync your Library" }]);
6360
+ printNext([{ command: cliCmd("login"), description: "see and sync your Library" }]);
6223
6361
  return;
6224
6362
  }
6225
6363
  const me = await fetchMe().catch(() => null);
@@ -6275,7 +6413,7 @@ async function statusCommand(rawOpts = {}) {
6275
6413
  };
6276
6414
  }),
6277
6415
  skillsNeedingAttention: buildAttention(checks),
6278
- next: ["floom sync", "floom status --verbose"]
6416
+ next: [cliCmd("sync"), cliCmd("status --verbose")]
6279
6417
  });
6280
6418
  process.exitCode = complete ? 0 : 1;
6281
6419
  return;
@@ -6291,8 +6429,8 @@ async function statusCommand(rawOpts = {}) {
6291
6429
  }
6292
6430
  }
6293
6431
  printNext([
6294
- { command: "floom push", description: "publish local changes" },
6295
- { command: "floom pull", description: "install Library updates" }
6432
+ { command: cliCmd("push"), description: "publish local changes" },
6433
+ { command: cliCmd("pull"), description: "install Library updates" }
6296
6434
  ]);
6297
6435
  return;
6298
6436
  }
@@ -6301,14 +6439,14 @@ async function statusCommand(rawOpts = {}) {
6301
6439
  if (attention.length === 0 && timedOut.length === 0) {
6302
6440
  log.blank();
6303
6441
  log.info(`All ${skillCount} skill${skillCount === 1 ? "" : "s"} are up to date in ${selectedAgents.map((a) => AGENT_LABELS[a]).join(", ")}.`);
6304
- printNext([{ command: "floom push", description: "if you just created a new skill locally" }]);
6442
+ printNext([{ command: cliCmd("push"), description: "if you just created a new skill locally" }]);
6305
6443
  return;
6306
6444
  }
6307
6445
  log.heading("Skills needing attention");
6308
6446
  log.blank();
6309
6447
  const shown = attention.slice(0, MAX_ATTENTION_ROWS);
6310
6448
  for (const item of shown) {
6311
- log.info(` ${chalk5.bold(item.slug)}`);
6449
+ log.info(` ${chalk6.bold(item.slug)}`);
6312
6450
  for (const loc of item.locations) {
6313
6451
  const c = toFloomLabel(loc.state);
6314
6452
  log.info(` ${AGENT_LABELS[loc.agent].padEnd(10)} ${c.color(c.label.padEnd(22))}${tildePath(loc.path)}`);
@@ -6316,7 +6454,7 @@ async function statusCommand(rawOpts = {}) {
6316
6454
  }
6317
6455
  if (attention.length > shown.length) {
6318
6456
  log.blank();
6319
- log.info(`...and ${attention.length - shown.length} more \u2014 run floom status --verbose.`);
6457
+ log.info(`...and ${attention.length - shown.length} more \u2014 run ${cliCmd("status --verbose")}.`);
6320
6458
  }
6321
6459
  const upToDate = checks.flatMap((c) => c.skills).filter((s) => s.state === "up_to_date").length;
6322
6460
  if (upToDate > 0) {
@@ -6327,15 +6465,15 @@ async function statusCommand(rawOpts = {}) {
6327
6465
  if (timedOut.length > 0) {
6328
6466
  log.blank();
6329
6467
  log.info("Status summary");
6330
- log.info(` ${timedOut.length} agent${timedOut.length === 1 ? "" : "s"} timed out \u2014 run floom status --agent ${timedOut[0].agent} to retry.`);
6468
+ log.info(` ${timedOut.length} agent${timedOut.length === 1 ? "" : "s"} timed out \u2014 run ${cliCmd(`status --agent ${timedOut[0].agent}`)} to retry.`);
6331
6469
  }
6332
6470
  const hasLocalUnpublished = attention.some((a) => a.locations.some((l) => l.state === "not_in_library_never_published" || l.state === "local_changes"));
6333
6471
  const hasUpdates = attention.some((a) => a.locations.some((l) => l.state === "update_available"));
6334
6472
  const next = [];
6335
- if (hasLocalUnpublished && hasUpdates) next.push({ command: "floom sync", description: "review local changes and Library updates together" });
6336
- if (hasLocalUnpublished) next.push({ command: "floom push", description: "publish local skills to the Library" });
6337
- if (hasUpdates) next.push({ command: "floom pull", description: "install Library updates" });
6338
- next.push({ command: "floom status --verbose", description: "see every skill" });
6473
+ if (hasLocalUnpublished && hasUpdates) next.push({ command: cliCmd("sync"), description: "review local changes and Library updates together" });
6474
+ if (hasLocalUnpublished) next.push({ command: cliCmd("push"), description: "publish local skills to the Library" });
6475
+ if (hasUpdates) next.push({ command: cliCmd("pull"), description: "install Library updates" });
6476
+ next.push({ command: cliCmd("status --verbose"), description: "see every skill" });
6339
6477
  printNext(next);
6340
6478
  if (timedOut.length > 0) process.exitCode = 1;
6341
6479
  }
@@ -6385,13 +6523,13 @@ function summarize(skills) {
6385
6523
  function toFloomLabel(state) {
6386
6524
  switch (state) {
6387
6525
  case "up_to_date":
6388
- return { label: "up to date", color: chalk5.green };
6526
+ return { label: "up to date", color: chalk6.green };
6389
6527
  case "local_changes":
6390
- return { label: "local changes", color: chalk5.yellow };
6528
+ return { label: "local changes", color: chalk6.yellow };
6391
6529
  case "update_available":
6392
- return { label: "update available", color: chalk5.yellow };
6530
+ return { label: "update available", color: chalk6.yellow };
6393
6531
  case "changed_in_two_places":
6394
- return { label: "changed in two places", color: chalk5.red };
6532
+ return { label: "changed in two places", color: chalk6.red };
6395
6533
  case "not_in_library_never_published":
6396
6534
  return { label: "not in Library", color: (s) => s };
6397
6535
  case "not_in_library_removed":
@@ -6430,7 +6568,7 @@ var MACHINE_FILE3 = join11(CONFIG_DIR3, "machine.json");
6430
6568
  async function renameDeviceCommand(newLabel) {
6431
6569
  const trimmed = (newLabel ?? "").trim().slice(0, 80);
6432
6570
  if (!trimmed) {
6433
- log.err('Device name cannot be empty. Use: floom rename-device "Work Laptop"');
6571
+ log.err(`Device name cannot be empty. Use: ${cliCmd('rename-device "Work Laptop"')}`);
6434
6572
  process.exitCode = 2;
6435
6573
  return;
6436
6574
  }
@@ -6451,8 +6589,8 @@ async function renameDeviceCommand(newLabel) {
6451
6589
  log.blank();
6452
6590
  log.info("Server sync will happen after your next login.");
6453
6591
  printNext([
6454
- { command: "floom login", description: "sync the device name to your workspace" },
6455
- { command: "floom account", description: "see account details including device name" }
6592
+ { command: cliCmd("login"), description: "sync the device name to your workspace" },
6593
+ { command: cliCmd("account"), description: "see account details including device name" }
6456
6594
  ]);
6457
6595
  return;
6458
6596
  }
@@ -6469,7 +6607,7 @@ async function renameDeviceCommand(newLabel) {
6469
6607
  }
6470
6608
  log.blank();
6471
6609
  log.info("This name appears in your workspace when teammates look at which machine synced last.");
6472
- printNext([{ command: "floom account", description: "see account details including device name" }]);
6610
+ printNext([{ command: cliCmd("account"), description: "see account details including device name" }]);
6473
6611
  }
6474
6612
 
6475
6613
  // src/commands/install.ts
@@ -6646,7 +6784,7 @@ async function installCommand(input, rawOpts = {}) {
6646
6784
  const detected = await detectAgents();
6647
6785
  if (detected.length === 0) {
6648
6786
  if (json) {
6649
- emitJson({ share: shareInfo(shareData), mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], next: ["floom install <link> --agent claude"] });
6787
+ emitJson({ share: shareInfo(shareData), mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], next: [cliCmd("install <link> --agent claude")] });
6650
6788
  } else {
6651
6789
  log.heading("Shared skill");
6652
6790
  log.info(` ${shareData.skill.title} ${shareData.skill.version.display}`);
@@ -6667,16 +6805,16 @@ async function installCommand(input, rawOpts = {}) {
6667
6805
  if (!planMode && selectedAgents.length === 0) {
6668
6806
  if (!isInteractive()) {
6669
6807
  if (json) {
6670
- emitJson({ share: shareInfo(shareData), error: "floom install needs to know where to install", next: ["floom install <link> --all-agents --yes"] });
6808
+ emitJson({ share: shareInfo(shareData), error: `${cliCmd("install")} needs to know where to install`, next: [cliCmd("install <link> --all-agents --yes")] });
6671
6809
  } else {
6672
- log.err("floom install needs to know where to install.");
6810
+ log.err(`${cliCmd("install")} needs to know where to install.`);
6673
6811
  log.blank();
6674
6812
  log.info("Pass --agent <name>, repeat --agent, or use --all-agents.");
6675
6813
  log.blank();
6676
6814
  log.info("Examples:");
6677
- log.command("floom install <link> --agent claude --yes");
6678
- log.command("floom install <link> --agent claude,codex --yes");
6679
- log.command("floom install <link> --all-agents --yes");
6815
+ log.command(cliCmd("install <link> --agent claude --yes"));
6816
+ log.command(cliCmd("install <link> --agent claude,codex --yes"));
6817
+ log.command(cliCmd("install <link> --all-agents --yes"));
6680
6818
  }
6681
6819
  process.exitCode = 2;
6682
6820
  return;
@@ -6745,7 +6883,7 @@ async function installCommand(input, rawOpts = {}) {
6745
6883
  wouldMutate: planAgents.length > 0,
6746
6884
  hasSkipped: false,
6747
6885
  agents: planAgents,
6748
- next: ["floom install <link> --all-agents --yes"]
6886
+ next: [cliCmd("install <link> --all-agents --yes")]
6749
6887
  });
6750
6888
  } else {
6751
6889
  log.heading("Install plan");
@@ -6806,7 +6944,7 @@ async function installCommand(input, rawOpts = {}) {
6806
6944
  backupPath: null,
6807
6945
  message: r.message
6808
6946
  })),
6809
- next: ["floom status"]
6947
+ next: [cliCmd("status")]
6810
6948
  });
6811
6949
  } else {
6812
6950
  printNext([{ command: `Open your agent and ask it to use "${shareData.skill.title}".` }]);
@@ -6947,10 +7085,10 @@ async function newCommand(name, rawOpts = {}) {
6947
7085
  const json = flags.json;
6948
7086
  if (flags.allScopes) {
6949
7087
  if (json) {
6950
- emitJson({ skill: { slug: name, name: deriveTitle(name) }, mode: "plan", applied: false, wouldMutate: false, scope: null, path: null, action: "refused", reason: "needs_agent_choice", message: "floom new creates one skill in one location; --all-scopes does not apply.", next: [`floom new ${name} --agent claude`] });
7088
+ emitJson({ skill: { slug: name, name: deriveTitle(name) }, mode: "plan", applied: false, wouldMutate: false, scope: null, path: null, action: "refused", reason: "needs_agent_choice", message: `${cliCmd("new")} creates one skill in one location; --all-scopes does not apply.`, next: [cliCmd(`new ${name} --agent claude`)] });
6951
7089
  } else {
6952
- log.err("floom new creates one skill in one location; --all-scopes does not apply.");
6953
- printNext([{ command: `floom new ${name} --agent claude` }]);
7090
+ log.err(`${cliCmd("new")} creates one skill in one location; --all-scopes does not apply.`);
7091
+ printNext([{ command: cliCmd(`new ${name} --agent claude`) }]);
6954
7092
  }
6955
7093
  process.exitCode = 2;
6956
7094
  return;
@@ -6963,7 +7101,7 @@ async function newCommand(name, rawOpts = {}) {
6963
7101
  log.blank();
6964
7102
  log.info("Skill names use lowercase letters, numbers, and hyphens only.");
6965
7103
  log.info("Use:");
6966
- log.command(`floom new ${name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")}`);
7104
+ log.command(cliCmd(`new ${name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")}`));
6967
7105
  }
6968
7106
  process.exitCode = 2;
6969
7107
  return;
@@ -6980,8 +7118,8 @@ async function newCommand(name, rawOpts = {}) {
6980
7118
  log.info(`Floom will not create a nested skill inside ${tildePath(cwd)}.`);
6981
7119
  printNext([
6982
7120
  { command: "cd ~/.claude/skills" },
6983
- { command: `floom new ${name}` },
6984
- { command: `floom new ${name} --agent claude` }
7121
+ { command: cliCmd(`new ${name}`) },
7122
+ { command: cliCmd(`new ${name} --agent claude`) }
6985
7123
  ]);
6986
7124
  }
6987
7125
  process.exitCode = 2;
@@ -7014,7 +7152,7 @@ async function newCommand(name, rawOpts = {}) {
7014
7152
  usesScannedDir = false;
7015
7153
  } else if (!isInteractive() || json) {
7016
7154
  if (json) {
7017
- emitJson({ skill: { slug: name, name: title }, mode: "plan", applied: false, wouldMutate: true, scope: null, path: null, action: "refused", reason: "needs_agent_choice", message: "floom new needs to know where to create the skill. Pass --agent <name>.", next: [`floom new ${name} --agent ${detected[0]}`] });
7155
+ emitJson({ skill: { slug: name, name: title }, mode: "plan", applied: false, wouldMutate: true, scope: null, path: null, action: "refused", reason: "needs_agent_choice", message: `${cliCmd("new")} needs to know where to create the skill. Pass --agent <name>.`, next: [cliCmd(`new ${name} --agent ${detected[0]}`)] });
7018
7156
  process.exitCode = 2;
7019
7157
  return;
7020
7158
  }
@@ -7023,8 +7161,8 @@ async function newCommand(name, rawOpts = {}) {
7023
7161
  scope = "global";
7024
7162
  usesScannedDir = true;
7025
7163
  } else {
7026
- log.err("floom new needs to know where to create the skill.");
7027
- printNext([{ command: `floom new ${name} --agent ${detected[0]}` }]);
7164
+ log.err(`${cliCmd("new")} needs to know where to create the skill.`);
7165
+ printNext([{ command: cliCmd(`new ${name} --agent ${detected[0]}`) }]);
7028
7166
  process.exitCode = 2;
7029
7167
  return;
7030
7168
  }
@@ -7057,7 +7195,7 @@ async function newCommand(name, rawOpts = {}) {
7057
7195
  }
7058
7196
  if (flags.dryRun || json && !flags.yes) {
7059
7197
  if (json) {
7060
- emitJson({ skill: { slug: name, name: title }, mode: "plan", applied: false, wouldMutate: true, scope, path: skillFolder, action: "create", reason: null, message: `${name} would be created`, next: [usesScannedDir ? "floom push" : `floom push ${skillFolder}`] });
7198
+ emitJson({ skill: { slug: name, name: title }, mode: "plan", applied: false, wouldMutate: true, scope, path: skillFolder, action: "create", reason: null, message: `${name} would be created`, next: [usesScannedDir ? cliCmd("push") : cliCmd(`push ${skillFolder}`)] });
7061
7199
  } else {
7062
7200
  log.heading("Create plan");
7063
7201
  log.blank();
@@ -7078,7 +7216,7 @@ async function newCommand(name, rawOpts = {}) {
7078
7216
  await mkdir7(skillFolder, { recursive: true });
7079
7217
  await writeFile6(skillMd, skillTemplate(title));
7080
7218
  await writeFile6(join13(skillFolder, ".floomignore"), FLOOMIGNORE);
7081
- const pushCmd = usesScannedDir ? "floom push" : `floom push ${tildePath(skillFolder)}`;
7219
+ const pushCmd = usesScannedDir ? cliCmd("push") : cliCmd(`push ${tildePath(skillFolder)}`);
7082
7220
  if (json) {
7083
7221
  emitJson({ skill: { slug: name, name: title }, mode: "apply", applied: true, scope, path: skillFolder, result: "created", reason: null, message: `Created ${skillFolder}`, next: [pushCmd] });
7084
7222
  return;
@@ -7089,7 +7227,7 @@ async function newCommand(name, rawOpts = {}) {
7089
7227
  if (!usesScannedDir) {
7090
7228
  log.blank();
7091
7229
  log.info("No AI agents were detected, so Floom created the skill in this folder.");
7092
- log.info("Zero-arg `floom push` will not find this folder until an agent is installed.");
7230
+ log.info(`Zero-arg \`${cliCmd("push")}\` will not find this folder until an agent is installed.`);
7093
7231
  next.push({ command: `Edit ${tildePath(skillMd)}` });
7094
7232
  } else {
7095
7233
  next.push({ command: `Edit ${tildePath(skillMd)}` });
@@ -7150,20 +7288,20 @@ async function doctorCommand(opts = {}) {
7150
7288
  const auth = await readAuth();
7151
7289
  let authOk = false;
7152
7290
  if (!auth) {
7153
- checks.push({ name: "auth", status: "fail", message: "not signed in", fix: "floom login" });
7291
+ checks.push({ name: "auth", status: "fail", message: "not signed in", fix: cliCmd("login") });
7154
7292
  } else {
7155
7293
  try {
7156
7294
  const me = await fetchMe();
7157
7295
  authOk = true;
7158
7296
  checks.push({ name: "auth", status: "pass", message: `signed in as ${me.user.email}`, fix: null });
7159
7297
  } catch {
7160
- checks.push({ name: "auth", status: "fail", message: "session expired", fix: "floom login" });
7298
+ checks.push({ name: "auth", status: "fail", message: "session expired", fix: cliCmd("login") });
7161
7299
  }
7162
7300
  }
7163
7301
  if (authOk) {
7164
7302
  checks.push({ name: "api", status: "pass", message: "https://floom.dev reachable", fix: null });
7165
7303
  } else {
7166
- checks.push({ name: "api", status: "warn", message: "skipped \u2014 sign in first", fix: "floom login" });
7304
+ checks.push({ name: "api", status: "warn", message: "skipped \u2014 sign in first", fix: cliCmd("login") });
7167
7305
  }
7168
7306
  const agents = await detectAgents();
7169
7307
  if (agents.length === 0) {
@@ -7186,7 +7324,7 @@ async function doctorCommand(opts = {}) {
7186
7324
  }
7187
7325
  }
7188
7326
  if (manifestCorrupt && corruptAgent) {
7189
- const fix = authOk ? `floom sync --dry-run --agent ${corruptAgent}` : "floom login";
7327
+ const fix = authOk ? cliCmd(`sync --dry-run --agent ${corruptAgent}`) : cliCmd("login");
7190
7328
  checks.push({
7191
7329
  name: "manifests",
7192
7330
  status: "fail",
@@ -7221,7 +7359,7 @@ async function doctorCommand(opts = {}) {
7221
7359
  name: "backups",
7222
7360
  status: "warn",
7223
7361
  message: `${formatBytes(backupBytes)} across detected agents`,
7224
- fix: "floom restore --list"
7362
+ fix: cliCmd("restore --list")
7225
7363
  });
7226
7364
  } else {
7227
7365
  checks.push({ name: "backups", status: "pass", message: `${formatBytes(backupBytes)} across ${agents.length} agent${agents.length === 1 ? "" : "s"}`, fix: null });
@@ -7241,7 +7379,7 @@ async function doctorCommand(opts = {}) {
7241
7379
  emitJson({
7242
7380
  ok,
7243
7381
  checks,
7244
- next: failures.length > 0 ? failures.map((c) => c.fix).filter((f) => Boolean(f)) : ["floom status"]
7382
+ next: failures.length > 0 ? failures.map((c) => c.fix).filter((f) => Boolean(f)) : [cliCmd("status")]
7245
7383
  });
7246
7384
  process.exitCode = ok ? 0 : 1;
7247
7385
  return;
@@ -7271,13 +7409,13 @@ async function doctorCommand(opts = {}) {
7271
7409
  if (authFail && manifestCorrupt) {
7272
7410
  log.blank();
7273
7411
  log.info("Fix in this order:");
7274
- log.command("floom login");
7275
- if (corruptAgent) log.command(`floom sync --dry-run --agent ${corruptAgent}`);
7412
+ log.command(cliCmd("login"));
7413
+ if (corruptAgent) log.command(cliCmd(`sync --dry-run --agent ${corruptAgent}`));
7276
7414
  } else {
7277
- printNext(failures.map((c) => ({ command: c.fix ?? "floom status" })));
7415
+ printNext(failures.map((c) => ({ command: c.fix ?? cliCmd("status") })));
7278
7416
  }
7279
7417
  } else {
7280
- printNext([{ command: "floom status" }]);
7418
+ printNext([{ command: cliCmd("status") }]);
7281
7419
  }
7282
7420
  process.exitCode = ok ? 0 : 1;
7283
7421
  }
@@ -7386,7 +7524,7 @@ async function diffCommand(skill, opts = {}) {
7386
7524
  lastSyncedHash: null,
7387
7525
  files: files.map((f) => ({ path: f, status: "added", binary: false }))
7388
7526
  }],
7389
- next: [`floom push ${copy.path}`]
7527
+ next: [cliCmd(`push ${copy.path}`)]
7390
7528
  });
7391
7529
  return;
7392
7530
  }
@@ -7398,7 +7536,7 @@ async function diffCommand(skill, opts = {}) {
7398
7536
  log.blank();
7399
7537
  log.info("Baseline: empty");
7400
7538
  log.info(`Local copy: ${tildePath(copy.path)}`);
7401
- printNext([{ command: `floom push ${tildePath(copy.path)}`, description: `publish ${skill} as v1` }]);
7539
+ printNext([{ command: cliCmd(`push ${tildePath(copy.path)}`), description: `publish ${skill} as v1` }]);
7402
7540
  return;
7403
7541
  }
7404
7542
  const localHash = await skillContentHash(copy.path) ?? "";
@@ -7417,7 +7555,7 @@ async function diffCommand(skill, opts = {}) {
7417
7555
  lastSyncedHash: entry.hash,
7418
7556
  files: []
7419
7557
  }],
7420
- next: ["floom status"]
7558
+ next: [cliCmd("status")]
7421
7559
  });
7422
7560
  return;
7423
7561
  }
@@ -7427,17 +7565,17 @@ async function diffCommand(skill, opts = {}) {
7427
7565
  log.info(`Last synced at v${entry.version_seq}.`);
7428
7566
  log.info(`Local copy: ${tildePath(copy.path)}`);
7429
7567
  log.blank();
7430
- log.info("Run floom status when back online for a full file-by-file comparison.");
7431
- printNext([{ command: "floom status" }]);
7568
+ log.info(`Run ${cliCmd("status")} when back online for a full file-by-file comparison.`);
7569
+ printNext([{ command: cliCmd("status") }]);
7432
7570
  return;
7433
7571
  }
7434
7572
  if (copies.length === 0) {
7435
7573
  if (json) {
7436
- emitJson({ skill, agent: agentArg ?? null, source: "library", library: { available: libraryAvailable, version: libraryVersion, hash: null }, agents: [], next: libraryAvailable ? ["floom pull"] : [] });
7574
+ emitJson({ skill, agent: agentArg ?? null, source: "library", library: { available: libraryAvailable, version: libraryVersion, hash: null }, agents: [], next: libraryAvailable ? [cliCmd("pull")] : [] });
7437
7575
  } else {
7438
7576
  log.info(`No local copy of ${skill} found on this machine.`);
7439
7577
  if (libraryAvailable) {
7440
- printNext([{ command: "floom pull", description: `install ${skill} from the Library` }]);
7578
+ printNext([{ command: cliCmd("pull"), description: `install ${skill} from the Library` }]);
7441
7579
  } else {
7442
7580
  log.err(`${skill} is not in the Library and has no local copy.`);
7443
7581
  }
@@ -7493,7 +7631,7 @@ async function diffCommand(skill, opts = {}) {
7493
7631
  source: libraryAvailable ? "library" : "last_synced_manifest",
7494
7632
  library: { available: libraryAvailable, version: libraryVersion, hash: null },
7495
7633
  agents: agentResults,
7496
- next: ["floom status"]
7634
+ next: [cliCmd("status")]
7497
7635
  });
7498
7636
  return;
7499
7637
  }
@@ -7522,10 +7660,10 @@ async function diffCommand(skill, opts = {}) {
7522
7660
  const conflict = agentResults.find((r) => r.state === "changed_in_two_places");
7523
7661
  const next = [];
7524
7662
  if (conflict) {
7525
- next.push({ command: `floom pull ${skill} --agent ${conflict.agent}${conflict.scope === "project" ? " --project" : ""}`, description: "use the Library version (your local copy is backed up first)" });
7526
- next.push({ command: `floom push ${tildePath(conflict.path)}`, description: "publish your local version" });
7663
+ next.push({ command: cliCmd(`pull ${skill} --agent ${conflict.agent}${conflict.scope === "project" ? " --project" : ""}`), description: "use the Library version (your local copy is backed up first)" });
7664
+ next.push({ command: cliCmd(`push ${tildePath(conflict.path)}`), description: "publish your local version" });
7527
7665
  } else {
7528
- next.push({ command: "floom status" });
7666
+ next.push({ command: cliCmd("status") });
7529
7667
  }
7530
7668
  printNext(next);
7531
7669
  void sep6;
@@ -7619,7 +7757,7 @@ async function restoreCommand(skill, opts = {}) {
7619
7757
  wouldMutate: false,
7620
7758
  selectedBackup: null,
7621
7759
  message: `${all.length} backups available`,
7622
- next: skill ? [`floom restore ${skill} --agent ${detected[0] ?? "claude"}`] : []
7760
+ next: skill ? [cliCmd(`restore ${skill} --agent ${detected[0] ?? "claude"}`)] : []
7623
7761
  });
7624
7762
  return;
7625
7763
  }
@@ -7641,12 +7779,12 @@ async function restoreCommand(skill, opts = {}) {
7641
7779
  log.info(` ${b.skill.padEnd(16)}${b.timestamp.padEnd(24)}${tildePath(b.path)}`);
7642
7780
  }
7643
7781
  if (all[0]) {
7644
- printNext([{ command: `floom restore ${all[0].skill} --agent ${all[0].agent}` }]);
7782
+ printNext([{ command: cliCmd(`restore ${all[0].skill} --agent ${all[0].agent}`) }]);
7645
7783
  }
7646
7784
  return;
7647
7785
  }
7648
7786
  if (!skill) {
7649
- if (!json) log.err("floom restore needs a skill name, or use floom restore --list to see backups.");
7787
+ if (!json) log.err(`${cliCmd("restore")} needs a skill name, or use ${cliCmd("restore --list")} to see backups.`);
7650
7788
  process.exitCode = 2;
7651
7789
  return;
7652
7790
  }
@@ -7664,10 +7802,10 @@ async function restoreCommand(skill, opts = {}) {
7664
7802
  const backups = (await listBackups(searchAgents)).filter((b) => b.skill === skill);
7665
7803
  if (backups.length === 0) {
7666
7804
  if (json) {
7667
- emitJson({ mode: "plan", applied: false, skill: { slug: skill }, agent, scope: null, availableBackups: [], wouldMutate: false, selectedBackup: null, message: `No backups found for ${skill}.`, next: ["floom restore --list"] });
7805
+ emitJson({ mode: "plan", applied: false, skill: { slug: skill }, agent, scope: null, availableBackups: [], wouldMutate: false, selectedBackup: null, message: `No backups found for ${skill}.`, next: [cliCmd("restore --list")] });
7668
7806
  } else {
7669
7807
  log.err(`No backups found for ${skill}.`);
7670
- printNext([{ command: "floom restore --list" }]);
7808
+ printNext([{ command: cliCmd("restore --list") }]);
7671
7809
  }
7672
7810
  process.exitCode = 2;
7673
7811
  return;
@@ -7692,7 +7830,7 @@ async function restoreCommand(skill, opts = {}) {
7692
7830
  }
7693
7831
  }
7694
7832
  if (opts.yes && !opts.dryRun && !agentArg && !isInteractive()) {
7695
- if (!json) log.err("floom restore needs --agent <name> --backup <timestamp> --yes for non-interactive restore.");
7833
+ if (!json) log.err(`${cliCmd("restore")} needs --agent <name> --backup <timestamp> --yes for non-interactive restore.`);
7696
7834
  process.exitCode = 2;
7697
7835
  return;
7698
7836
  }
@@ -7712,7 +7850,7 @@ async function restoreCommand(skill, opts = {}) {
7712
7850
  wouldMutate: true,
7713
7851
  selectedBackup: selected?.timestamp ?? null,
7714
7852
  message: `${skill} would be restored from ${chosen.timestamp}`,
7715
- next: [`floom restore ${skill} --agent ${agent} --backup ${chosen.timestamp} --yes`]
7853
+ next: [cliCmd(`restore ${skill} --agent ${agent} --backup ${chosen.timestamp} --yes`)]
7716
7854
  });
7717
7855
  } else {
7718
7856
  log.heading("Restore plan");
@@ -7742,8 +7880,8 @@ async function restoreCommand(skill, opts = {}) {
7742
7880
  }
7743
7881
  selected = agentBackups[choice - 1];
7744
7882
  } else {
7745
- log.err(`floom restore needs a backup timestamp. Pass --backup <timestamp>, or run interactively.`);
7746
- log.info("Run floom restore --list to see available backups.");
7883
+ log.err(`${cliCmd("restore")} needs a backup timestamp. Pass --backup <timestamp>, or run interactively.`);
7884
+ log.info(`Run ${cliCmd("restore --list")} to see available backups.`);
7747
7885
  process.exitCode = 2;
7748
7886
  return;
7749
7887
  }
@@ -7787,15 +7925,15 @@ async function restoreCommand(skill, opts = {}) {
7787
7925
  preRestoreBackupPath: preRestoreBackup,
7788
7926
  result: "restored",
7789
7927
  message: `Restored ${skill} from ${selected.timestamp}`,
7790
- next: [`floom status --agent ${agent}`]
7928
+ next: [cliCmd(`status --agent ${agent}`)]
7791
7929
  });
7792
7930
  return;
7793
7931
  }
7794
7932
  log.ok(`Restored ${skill} from ${selected.timestamp}`);
7795
- printNext([{ command: `floom status --agent ${agent}` }]);
7933
+ printNext([{ command: cliCmd(`status --agent ${agent}`) }]);
7796
7934
  } catch (error) {
7797
7935
  if (json) {
7798
- emitJson({ mode: "apply", applied: true, skill: { slug: skill }, agent, scope: selected.scope, backup: selected.timestamp, preRestoreBackupPath: null, result: "failed", message: error.message, next: ["floom restore --list"] });
7936
+ emitJson({ mode: "apply", applied: true, skill: { slug: skill }, agent, scope: selected.scope, backup: selected.timestamp, preRestoreBackupPath: null, result: "failed", message: error.message, next: [cliCmd("restore --list")] });
7799
7937
  } else {
7800
7938
  log.err(error.message);
7801
7939
  }
@@ -7821,7 +7959,7 @@ async function dashboardCommand() {
7821
7959
  }
7822
7960
  log.blank();
7823
7961
  log.info("Not signed in \u2014 can't compare with your team Library.");
7824
- printNext([{ command: "floom login", description: "sign in to see and sync your Library" }]);
7962
+ printNext([{ command: cliCmd("login"), description: "sign in to see and sync your Library" }]);
7825
7963
  return;
7826
7964
  }
7827
7965
  const me = await fetchMe().catch(() => null);
@@ -7891,23 +8029,23 @@ async function dashboardCommand() {
7891
8029
  }
7892
8030
  const next = [];
7893
8031
  if (anyLocalChanges && anyUpdates) {
7894
- next.push({ command: "floom sync", description: "review local changes and Library updates together" });
7895
- next.push({ command: "floom push", description: "publish local skills to the Library" });
7896
- next.push({ command: "floom pull", description: "update agent copies from the Library" });
8032
+ next.push({ command: cliCmd("sync"), description: "review local changes and Library updates together" });
8033
+ next.push({ command: cliCmd("push"), description: "publish local skills to the Library" });
8034
+ next.push({ command: cliCmd("pull"), description: "update agent copies from the Library" });
7897
8035
  } else if (anyUpdates) {
7898
- next.push({ command: "floom pull", description: "update agent copies from the Library" });
8036
+ next.push({ command: cliCmd("pull"), description: "update agent copies from the Library" });
7899
8037
  } else if (anyLocalChanges) {
7900
- next.push({ command: "floom push", description: "publish local skills to the Library" });
8038
+ next.push({ command: cliCmd("push"), description: "publish local skills to the Library" });
7901
8039
  } else if (!libResult.ok) {
7902
- next.push({ command: "floom status", description: "retry full comparison" });
8040
+ next.push({ command: cliCmd("status"), description: "retry full comparison" });
7903
8041
  }
7904
- next.push({ command: "floom status", description: "see full detail" });
8042
+ next.push({ command: cliCmd("status"), description: "see full detail" });
7905
8043
  printNext(next);
7906
8044
  void [];
7907
8045
  }
7908
8046
 
7909
8047
  // src/lib/help.ts
7910
- import chalk6 from "chalk";
8048
+ import chalk7 from "chalk";
7911
8049
  var GROUPS = [
7912
8050
  {
7913
8051
  title: "Sign in",
@@ -7977,21 +8115,21 @@ var COMMON_FLAGS = `Common flags
7977
8115
  --no-secret-check with push, skip the pre-publish secret scan`;
7978
8116
  function printGroupedHelp() {
7979
8117
  const out2 = process.stdout;
7980
- out2.write("\n" + chalk6.bold("Usage: floom [command]") + "\n\n");
7981
- out2.write(` ${chalk6.cyan.bold("floom")} your dashboard \u2014 Library and agent status at a glance
8118
+ out2.write("\n" + chalk7.bold("Usage: floom [command]") + "\n\n");
8119
+ out2.write(` ${chalk7.cyan.bold("floom")} your dashboard \u2014 Library and agent status at a glance
7982
8120
  `);
7983
8121
  const allNames = GROUPS.flatMap((g) => g.rows.map((r) => r.name));
7984
8122
  const width = Math.max(...allNames.map((n) => n.length), 14);
7985
8123
  for (const group of GROUPS) {
7986
- out2.write("\n" + chalk6.bold(group.title) + "\n");
8124
+ out2.write("\n" + chalk7.bold(group.title) + "\n");
7987
8125
  for (const row of group.rows) {
7988
- const aliasNote = row.alias ? chalk6.dim(` ${row.alias} (alias)`) : "";
7989
- out2.write(` ${chalk6.cyan.bold(row.name.padEnd(width + 2))}${row.description}${aliasNote}
8126
+ const aliasNote = row.alias ? chalk7.dim(` ${row.alias} (alias)`) : "";
8127
+ out2.write(` ${chalk7.cyan.bold(row.name.padEnd(width + 2))}${row.description}${aliasNote}
7990
8128
  `);
7991
8129
  }
7992
8130
  }
7993
8131
  out2.write("\n" + COMMON_FLAGS + "\n");
7994
- out2.write("\n" + chalk6.dim("More help: https://floom.dev/docs") + "\n");
8132
+ out2.write("\n" + chalk7.dim("More help: https://floom.dev/docs") + "\n");
7995
8133
  }
7996
8134
 
7997
8135
  // src/index.ts
@@ -8054,7 +8192,7 @@ function editDistance(a, b) {
8054
8192
  return curr[b.length];
8055
8193
  }
8056
8194
  helpOpt(program.command("login").description("sign in to your Floom workspace")).action(loginCommand);
8057
- helpOpt(program.command("logout").description("sign out on this machine")).action(logoutCommand);
8195
+ helpOpt(program.command("logout").description("sign out on this machine").option("--all", "sign out all CLI sessions for this account")).action((opts) => logoutCommand(opts));
8058
8196
  helpOpt(program.command("account").description("show account details").option("--json", "print account state as JSON")).action((opts) => accountCommand(opts));
8059
8197
  helpOpt(program.command("whoami").description("show account details (alias)").option("--json", "print account state as JSON")).action((opts) => {
8060
8198
  aliasNotice("whoami", "account");