@floomhq/floom 3.0.3 → 3.1.0

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,11 +3067,38 @@ 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
- var DEFAULT_APP_URL = "https://floom.dev";
3073
- var DEFAULT_API_URL = "https://floom.dev/api/v1";
3074
- var TRUSTED_API_HOSTS = /* @__PURE__ */ new Set(["floom.dev", "try.floom.dev"]);
3099
+ var DEFAULT_APP_URL = "https://skills.floom.dev";
3100
+ var DEFAULT_API_URL = "https://skills.floom.dev/api/v1";
3101
+ var TRUSTED_API_HOSTS = /* @__PURE__ */ new Set(["floom.dev", "skills.floom.dev", "try.floom.dev"]);
3075
3102
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1"]);
3076
3103
  function devModeEnabled() {
3077
3104
  const v = process.env.FLOOM_DEV;
@@ -3097,7 +3124,9 @@ function trustedApiUrlOrDefault(apiUrl) {
3097
3124
  const trimmed = apiUrl.replace(/\/$/, "");
3098
3125
  if (!isTrustedApiUrl(trimmed)) return DEFAULT_API_URL;
3099
3126
  try {
3100
- if (TRUSTED_API_HOSTS.has(new URL(trimmed).hostname)) return DEFAULT_API_URL;
3127
+ const hostname2 = new URL(trimmed).hostname;
3128
+ if (hostname2 === "floom.dev" || hostname2 === "try.floom.dev") return DEFAULT_API_URL;
3129
+ if (hostname2 === "skills.floom.dev") return `${new URL(trimmed).origin}/api/v1`;
3101
3130
  } catch {
3102
3131
  return DEFAULT_API_URL;
3103
3132
  }
@@ -3119,18 +3148,25 @@ async function readRawAuth() {
3119
3148
  if (code === "ENOENT") return null;
3120
3149
  if (code === "EACCES" || code === "EPERM") {
3121
3150
  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"
3151
+ `Cannot read your Floom auth file (permission denied).
3152
+ Fix: chmod 600 ~/.floom/auth.json
3153
+ Or re-authenticate: ${cliCmd("login")}
3154
+ More help: https://floom.dev/docs#troubleshooting`
3123
3155
  );
3124
3156
  }
3125
3157
  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"
3158
+ `Cannot read your Floom auth file.
3159
+ Run: ${cliCmd("login")}
3160
+ More help: https://floom.dev/docs#troubleshooting`
3127
3161
  );
3128
3162
  }
3129
3163
  try {
3130
3164
  return JSON.parse(raw);
3131
3165
  } catch {
3132
3166
  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"
3167
+ `Your Floom auth file is corrupted.
3168
+ Run: ${cliCmd("login")} to generate a fresh one.
3169
+ More help: https://floom.dev/docs#troubleshooting`
3134
3170
  );
3135
3171
  }
3136
3172
  }
@@ -3253,7 +3289,7 @@ async function getMachineIdentity() {
3253
3289
  }
3254
3290
 
3255
3291
  // src/version.ts
3256
- var VERSION = "3.0.3";
3292
+ var VERSION = "3.0.4";
3257
3293
 
3258
3294
  // src/api-client.ts
3259
3295
  var DEFAULT_TIMEOUT_MS = 2e4;
@@ -3304,12 +3340,12 @@ var HELP = "https://floom.dev/docs#troubleshooting";
3304
3340
  function friendlyApiErrorMessage(code, raw, status) {
3305
3341
  if (code === "AUTH_REQUIRED" || code === "TOKEN_INVALID" || status === 401) {
3306
3342
  return `Not signed in.
3307
- Run: floom login
3343
+ Run: ${cliCmd("login")}
3308
3344
  More help: ${HELP}`;
3309
3345
  }
3310
3346
  if (code === "TOKEN_EXPIRED") {
3311
3347
  return `Your session has expired.
3312
- Run: floom login
3348
+ Run: ${cliCmd("login")}
3313
3349
  More help: ${HELP}`;
3314
3350
  }
3315
3351
  if (code === "SKILL_ACCESS_DENIED" || status === 403) {
@@ -3318,7 +3354,7 @@ function friendlyApiErrorMessage(code, raw, status) {
3318
3354
  }
3319
3355
  if (code === "SKILL_NOT_FOUND" || status === 404) {
3320
3356
  return `Skill not found. It may have been deleted or the slug is wrong.
3321
- Run: floom list
3357
+ Run: ${cliCmd("list")}
3322
3358
  More help: ${HELP}`;
3323
3359
  }
3324
3360
  if (code === "RATE_LIMITED" || status === 429) {
@@ -3346,7 +3382,9 @@ async function api(path, opts = {}) {
3346
3382
  if (opts.authRequired && !token) {
3347
3383
  throw new FloomError(
3348
3384
  "AUTH_REQUIRED",
3349
- "Not signed in.\n Run: floom login\n More help: https://floom.dev/docs#troubleshooting"
3385
+ `Not signed in.
3386
+ Run: ${cliCmd("login")}
3387
+ More help: https://floom.dev/docs#troubleshooting`
3350
3388
  );
3351
3389
  }
3352
3390
  let lastError = null;
@@ -3425,7 +3463,7 @@ async function api(path, opts = {}) {
3425
3463
  // src/lib/agents.ts
3426
3464
  init_src();
3427
3465
  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";
3466
+ import { readdir as readdir2, readFile as readFile5, readlink, stat as stat3, lstat as lstat2 } from "node:fs/promises";
3429
3467
  import { homedir as homedir4 } from "node:os";
3430
3468
  import { join as join5, posix as posix2, relative as relative3, resolve as resolve2, sep as sep3 } from "node:path";
3431
3469
  var AGENT_LABELS = {
@@ -3550,7 +3588,28 @@ async function collectHashFiles(root) {
3550
3588
  const abs = join5(dir, entry.name);
3551
3589
  const info = await lstat2(abs);
3552
3590
  if (info.isSymbolicLink()) {
3553
- throw new Error(`skill folders must not contain symlinks: ${abs}`);
3591
+ let target = "(unreadable)";
3592
+ try {
3593
+ target = await readlink(abs);
3594
+ } catch {
3595
+ }
3596
+ throw new Error(
3597
+ `Skill folder contains a symlink (not allowed for security):
3598
+ link: ${abs}
3599
+ target: ${target}
3600
+
3601
+ Why: a symlink could point outside the skill folder and quietly leak files
3602
+ (SSH keys, .env, cookie jars) into a public Library. Floom refuses to read
3603
+ through symlinks for that reason.
3604
+
3605
+ Fix: replace the link with the real file contents:
3606
+ rm "${abs}"
3607
+ cp "${target}" "${abs}"
3608
+
3609
+ (or move the target file under the skill folder if it lives elsewhere)
3610
+
3611
+ More help: https://floom.dev/docs#skill-folder-rules`
3612
+ );
3554
3613
  }
3555
3614
  if (entry.isDirectory()) {
3556
3615
  await walk2(abs, childRel);
@@ -3622,8 +3681,16 @@ function tildePath(path, homeDir = homedir4()) {
3622
3681
  }
3623
3682
 
3624
3683
  // src/commands/login.ts
3684
+ function isHeadlessLinuxSession(env = process.env, platform3 = process.platform) {
3685
+ if (env.FLOOM_NO_OPEN === "1") return true;
3686
+ if (platform3 !== "linux") return false;
3687
+ const isSsh = Boolean(env.SSH_CONNECTION ?? env.SSH_CLIENT ?? env.SSH_TTY);
3688
+ const hasDisplay = Boolean(env.DISPLAY ?? env.WAYLAND_DISPLAY);
3689
+ return isSsh || !hasDisplay;
3690
+ }
3625
3691
  function tryOpenBrowser(url) {
3626
3692
  if (process.env.FLOOM_NO_OPEN === "1") return;
3693
+ if (isHeadlessLinuxSession()) return;
3627
3694
  const platform3 = process.platform;
3628
3695
  let cmd;
3629
3696
  let args;
@@ -3666,8 +3733,17 @@ async function loginCommand() {
3666
3733
  });
3667
3734
  log.heading("Sign in to Floom");
3668
3735
  log.blank();
3669
- log.info(` Open: ${session.verification_uri}`);
3670
- log.info(` Code: ${chalk2.bold.cyan(session.user_code)}`);
3736
+ const headless = isHeadlessLinuxSession();
3737
+ if (headless) {
3738
+ log.info(" Open this URL in any browser to approve:");
3739
+ log.info(` ${session.verification_uri}`);
3740
+ log.info(` Code: ${chalk2.bold.cyan(session.user_code)}`);
3741
+ log.blank();
3742
+ log.info(" You can open it on your phone or laptop \u2014 the same URL works from anywhere.");
3743
+ } else {
3744
+ log.info(` Open: ${session.verification_uri}`);
3745
+ log.info(` Code: ${chalk2.bold.cyan(session.user_code)}`);
3746
+ }
3671
3747
  log.blank();
3672
3748
  process.stdout.write("Waiting for browser approval...");
3673
3749
  tryOpenBrowser(session.verification_uri);
@@ -3693,9 +3769,13 @@ async function loginCommand() {
3693
3769
  if (token.workspace?.name) log.info(` Workspace: ${token.workspace.name}`);
3694
3770
  await printAgentsFound();
3695
3771
  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" }
3772
+ { command: cliCmd("pull"), description: "install your team's skills into these agents" },
3773
+ { command: cliCmd("push"), description: "if you have skills here to publish first" }
3698
3774
  ]);
3775
+ if (cliCmd("login").startsWith("npx ")) {
3776
+ log.blank();
3777
+ log.info(" Tip: install once with npm i -g @floomhq/floom and just type `floom` after that.");
3778
+ }
3699
3779
  return;
3700
3780
  }
3701
3781
  } catch (error) {
@@ -3714,7 +3794,7 @@ async function loginCommand() {
3714
3794
  process.stdout.write("\n");
3715
3795
  log.err("Login timed out \u2014 the browser window expired.");
3716
3796
  printNext([
3717
- { command: "floom login", description: "try again" },
3797
+ { command: cliCmd("login"), description: "try again" },
3718
3798
  { command: "More help: https://floom.dev/docs#troubleshooting" }
3719
3799
  ]);
3720
3800
  process.exitCode = 1;
@@ -3752,7 +3832,7 @@ async function logoutCommand(opts = {}, deps = {}) {
3752
3832
  );
3753
3833
  log.blank();
3754
3834
  log.info("Your local skill files in ~/.claude/skills, ~/.codex/skills, etc. were not changed.");
3755
- printNext([{ command: "floom login", description: "sign in again" }]);
3835
+ printNext([{ command: cliCmd("login"), description: "sign in again" }]);
3756
3836
  }
3757
3837
 
3758
3838
  // src/commands/account.ts
@@ -3807,18 +3887,18 @@ async function accountCommand(opts = {}) {
3807
3887
  const device = await deviceLabel();
3808
3888
  if (!auth) {
3809
3889
  if (json) {
3810
- emitJson({ signedIn: false, email: null, workspace: null, device: device ? { label: device } : null, next: ["floom login"] });
3890
+ emitJson({ signedIn: false, email: null, workspace: null, device: device ? { label: device } : null, next: [cliCmd("login")] });
3811
3891
  return;
3812
3892
  }
3813
3893
  log.info("Not signed in to Floom.");
3814
- printNext([{ command: "floom login" }]);
3894
+ printNext([{ command: cliCmd("login") }]);
3815
3895
  return;
3816
3896
  }
3817
3897
  if (!json) {
3818
3898
  const rawAuth = await readRawAuth();
3819
3899
  if (rawAuth && !isTrustedApiUrl(rawAuth.apiUrl)) {
3820
3900
  log.warn(
3821
- `Your saved Floom credentials point at ${rawAuth.apiUrl}, which is not a trusted Floom API. Run 'floom login' to re-authenticate.`
3901
+ `Your saved Floom credentials point at ${rawAuth.apiUrl}, which is not a trusted Floom API. Run '${cliCmd("login")}' to re-authenticate.`
3822
3902
  );
3823
3903
  }
3824
3904
  }
@@ -3830,7 +3910,7 @@ async function accountCommand(opts = {}) {
3830
3910
  email: me.user.email ?? auth.email,
3831
3911
  workspace: me.workspace ? { name: me.workspace.name } : null,
3832
3912
  device: { label: device },
3833
- next: ["floom status", "floom logout"]
3913
+ next: [cliCmd("status"), cliCmd("logout")]
3834
3914
  });
3835
3915
  return;
3836
3916
  }
@@ -3840,19 +3920,19 @@ async function accountCommand(opts = {}) {
3840
3920
  if (me.workspace) log.kv("Workspace:", me.workspace.name);
3841
3921
  log.kv("This machine:", device ?? "not named yet");
3842
3922
  printNext([
3843
- { command: "floom status", description: "see Library and local agent copies" },
3844
- { command: "floom logout", description: "sign out on this machine" }
3923
+ { command: cliCmd("status"), description: "see Library and local agent copies" },
3924
+ { command: cliCmd("logout"), description: "sign out on this machine" }
3845
3925
  ]);
3846
3926
  } catch (error) {
3847
3927
  if (json) {
3848
- emitJson({ signedIn: false, email: auth.email, workspace: null, device: { label: device }, next: ["floom login"] });
3928
+ emitJson({ signedIn: false, email: auth.email, workspace: null, device: { label: device }, next: [cliCmd("login")] });
3849
3929
  process.exitCode = 1;
3850
3930
  return;
3851
3931
  }
3852
3932
  log.err(error.message);
3853
3933
  log.blank();
3854
3934
  log.info("Session expired. Sign in again to continue.");
3855
- printNext([{ command: "floom login" }]);
3935
+ printNext([{ command: cliCmd("login") }]);
3856
3936
  process.exitCode = 1;
3857
3937
  }
3858
3938
  void isJsonMode;
@@ -4720,7 +4800,7 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4720
4800
  syncedCount: 0,
4721
4801
  plan: [],
4722
4802
  error: { code: "AUTH_REQUIRED", message: "Not signed in \u2014 log in first to publish." },
4723
- next: ["floom login"]
4803
+ next: [cliCmd("login")]
4724
4804
  });
4725
4805
  process.exitCode = 1;
4726
4806
  return;
@@ -4730,7 +4810,7 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4730
4810
  log.blank();
4731
4811
  if (skills.length === 0) {
4732
4812
  log.info("No local skill folders found on this machine.");
4733
- printNext([{ command: "floom login" }]);
4813
+ printNext([{ command: cliCmd("login") }]);
4734
4814
  process.exitCode = 1;
4735
4815
  return;
4736
4816
  }
@@ -4738,10 +4818,10 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4738
4818
  for (const skill of skills) log.info(` ${tildePath(skill.path)}`);
4739
4819
  log.blank();
4740
4820
  log.info("To compare and publish them, sign in to Floom.");
4741
- printNext([{ command: "floom login" }]);
4821
+ printNext([{ command: cliCmd("login") }]);
4742
4822
  log.blank();
4743
4823
  log.info("After login, run:");
4744
- log.command("floom push");
4824
+ log.command(cliCmd("push"));
4745
4825
  process.exitCode = 1;
4746
4826
  return;
4747
4827
  }
@@ -4759,7 +4839,7 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4759
4839
  wouldMutate: false,
4760
4840
  syncedCount: 0,
4761
4841
  plan: [],
4762
- next: libCount > 0 ? ["floom pull"] : ["floom new"]
4842
+ next: libCount > 0 ? [cliCmd("pull")] : [cliCmd("new")]
4763
4843
  });
4764
4844
  return;
4765
4845
  }
@@ -4768,11 +4848,11 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4768
4848
  log.blank();
4769
4849
  if (libCount > 0) {
4770
4850
  log.info(`Your Library has ${libCount} skill${libCount === 1 ? "" : "s"}. To install them here, run:`);
4771
- log.command("floom pull");
4851
+ log.command(cliCmd("pull"));
4772
4852
  }
4773
4853
  printNext([
4774
- { command: "floom pull", description: "install your Library on this machine" },
4775
- { command: "floom new research-helper", description: "create a ready-to-edit skill" }
4854
+ { command: cliCmd("pull"), description: "install your Library on this machine" },
4855
+ { command: cliCmd("new research-helper"), description: "create a ready-to-edit skill" }
4776
4856
  ]);
4777
4857
  return;
4778
4858
  }
@@ -4815,7 +4895,7 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4815
4895
  lastSyncedLibraryVersion: r.lastSyncedLibraryVersion,
4816
4896
  publishVersion: r.publishVersion
4817
4897
  })),
4818
- next: ["floom push --yes", "floom push"]
4898
+ next: [cliCmd("push --yes"), cliCmd("push")]
4819
4899
  });
4820
4900
  process.exitCode = wouldMutate && !options.exitZero ? 1 : 0;
4821
4901
  return;
@@ -4837,7 +4917,7 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4837
4917
  if (!wouldMutate && refusedRows.length === 0) {
4838
4918
  log.blank();
4839
4919
  log.info("Everything is already in sync with the Library. Nothing to publish.");
4840
- printNext([{ command: "floom status", description: "see every copy" }]);
4920
+ printNext([{ command: cliCmd("status"), description: "see every copy" }]);
4841
4921
  return;
4842
4922
  }
4843
4923
  let toPublish = [];
@@ -4845,12 +4925,12 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4845
4925
  toPublish = safeRows;
4846
4926
  } else if (!isInteractive()) {
4847
4927
  log.blank();
4848
- log.err("floom push needs a choice but is not running in an interactive terminal.");
4928
+ log.err(`${cliCmd("push")} needs a choice but is not running in an interactive terminal.`);
4849
4929
  log.blank();
4850
4930
  log.info("Re-run with:");
4851
- log.command("floom push --dry-run preview what would happen");
4852
- log.command("floom push --yes proceed with all safe changes");
4853
- log.command("floom push --json get the plan as JSON");
4931
+ log.command(cliCmd("push --dry-run") + " preview what would happen");
4932
+ log.command(cliCmd("push --yes") + " proceed with all safe changes");
4933
+ log.command(cliCmd("push --json") + " get the plan as JSON");
4854
4934
  process.exitCode = 2;
4855
4935
  return;
4856
4936
  } else {
@@ -4900,9 +4980,9 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4900
4980
  }
4901
4981
  const conflictSlug = skipped.find((r) => r.action === "conflict")?.slug;
4902
4982
  printNext([
4903
- ...conflictSlug ? [{ command: `floom diff ${conflictSlug}`, description: "review the conflict before publishing" }] : [],
4904
- { command: "floom pull", description: "update your other agents with these new versions" },
4905
- { command: "floom status", description: "see every copy" }
4983
+ ...conflictSlug ? [{ command: cliCmd(`diff ${conflictSlug}`), description: "review the conflict before publishing" }] : [],
4984
+ { command: cliCmd("pull"), description: "update your other agents with these new versions" },
4985
+ { command: cliCmd("status"), description: "see every copy" }
4906
4986
  ]);
4907
4987
  if (failures.length > 0 || skipped.length > 0) process.exitCode = 1;
4908
4988
  } finally {
@@ -4915,10 +4995,10 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
4915
4995
  const info = await stat5(folder).catch(() => null);
4916
4996
  if (!info || !info.isDirectory()) {
4917
4997
  if (json) {
4918
- emitJson({ workspace: { name: "Library", signedIn: authed }, mode: "plan", applied: false, wouldMutate: false, syncedCount: 0, plan: [], next: ["floom push"] });
4998
+ emitJson({ workspace: { name: "Library", signedIn: authed }, mode: "plan", applied: false, wouldMutate: false, syncedCount: 0, plan: [], next: [cliCmd("push")] });
4919
4999
  } else {
4920
5000
  log.err(`Folder not found: ${pathArg}`);
4921
- log.info(" Run floom push with no arguments to auto-detect your skills, or pass a valid folder path.");
5001
+ log.info(` Run ${cliCmd("push")} with no arguments to auto-detect your skills, or pass a valid folder path.`);
4922
5002
  }
4923
5003
  process.exitCode = 2;
4924
5004
  return;
@@ -4941,13 +5021,13 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
4941
5021
  syncedCount: 0,
4942
5022
  plan: [],
4943
5023
  error: { code: "AUTH_REQUIRED", message: "Not signed in \u2014 log in first to publish." },
4944
- next: ["floom login"]
5024
+ next: [cliCmd("login")]
4945
5025
  });
4946
5026
  } else {
4947
5027
  log.blank();
4948
5028
  log.err("Not signed in \u2014 log in first to publish.");
4949
5029
  log.info(`Found local skill at ${tildePath(folder)}.`);
4950
- printNext([{ command: "floom login" }]);
5030
+ printNext([{ command: cliCmd("login") }]);
4951
5031
  }
4952
5032
  process.exitCode = 1;
4953
5033
  return;
@@ -4964,7 +5044,7 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
4964
5044
  wouldMutate: false,
4965
5045
  syncedCount: 0,
4966
5046
  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 }],
4967
- next: ["floom push --no-secret-check"]
5047
+ next: [cliCmd("push --no-secret-check")]
4968
5048
  });
4969
5049
  process.exitCode = 1;
4970
5050
  return;
@@ -4986,7 +5066,7 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
4986
5066
  wouldMutate: false,
4987
5067
  syncedCount: 0,
4988
5068
  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 }],
4989
- next: ["floom push --no-secret-check"]
5069
+ next: [cliCmd("push --no-secret-check")]
4990
5070
  });
4991
5071
  process.exitCode = 1;
4992
5072
  return;
@@ -5011,7 +5091,7 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
5011
5091
  log.info("Nothing published.");
5012
5092
  printNext([
5013
5093
  { command: "Edit .floomignore to exclude secrets or generated files" },
5014
- { command: `floom push ${pathArg}` }
5094
+ { command: cliCmd(`push ${pathArg}`) }
5015
5095
  ]);
5016
5096
  return;
5017
5097
  }
@@ -5026,7 +5106,7 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
5026
5106
  wouldMutate: true,
5027
5107
  syncedCount: 0,
5028
5108
  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 }],
5029
- next: ["floom push " + pathArg + " --yes"]
5109
+ next: [cliCmd(`push ${pathArg} --yes`)]
5030
5110
  });
5031
5111
  } else {
5032
5112
  log.heading("Push plan");
@@ -5046,15 +5126,15 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
5046
5126
  mode: "apply",
5047
5127
  applied: true,
5048
5128
  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 }],
5049
- next: ["floom pull"]
5129
+ next: [cliCmd("pull")]
5050
5130
  });
5051
5131
  } else {
5052
5132
  log.ok(`Published ${result.skill.slug} ${result.skill.latest.display}`);
5053
- printNext([{ command: "floom pull", description: `update your other agents with ${result.skill.slug}` }]);
5133
+ printNext([{ command: cliCmd("pull"), description: `update your other agents with ${result.skill.slug}` }]);
5054
5134
  }
5055
5135
  } catch (error) {
5056
5136
  if (json) {
5057
- 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] });
5137
+ 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}`)] });
5058
5138
  } else {
5059
5139
  log.err(error.message);
5060
5140
  }
@@ -5095,11 +5175,11 @@ async function deleteCommand(slug, opts = {}, deps = { api }) {
5095
5175
  skill: { slug: "", version: null, exists: false },
5096
5176
  wouldMutate: false,
5097
5177
  action: "missing_argument",
5098
- message: "floom delete needs a skill name.",
5099
- next: ["floom list"]
5178
+ message: `${cliCmd("delete")} needs a skill name.`,
5179
+ next: [cliCmd("list")]
5100
5180
  });
5101
5181
  } else {
5102
- log.err("floom delete needs a skill name. Run `floom list` to see skills in the Library.");
5182
+ log.err(`${cliCmd("delete")} needs a skill name. Run \`${cliCmd("list")}\` to see skills in the Library.`);
5103
5183
  }
5104
5184
  process.exitCode = 2;
5105
5185
  return;
@@ -5116,10 +5196,10 @@ async function deleteCommand(slug, opts = {}, deps = { api }) {
5116
5196
  wouldMutate: false,
5117
5197
  action: "not_found",
5118
5198
  message: `No skill named '${slug}' in ${ws}.`,
5119
- next: ["floom list"]
5199
+ next: [cliCmd("list")]
5120
5200
  });
5121
5201
  } else {
5122
- log.err(`No skill named '${slug}' in ${ws}. Run \`floom list\` to see what is in the Library.`);
5202
+ log.err(`No skill named '${slug}' in ${ws}. Run \`${cliCmd("list")}\` to see what is in the Library.`);
5123
5203
  }
5124
5204
  process.exitCode = 2;
5125
5205
  return;
@@ -5134,7 +5214,7 @@ async function deleteCommand(slug, opts = {}, deps = { api }) {
5134
5214
  wouldMutate: true,
5135
5215
  action: "delete",
5136
5216
  message: `${slug} would be deleted from the Library.`,
5137
- next: ["floom delete " + slug + " --yes"]
5217
+ next: [cliCmd(`delete ${slug} --yes`)]
5138
5218
  });
5139
5219
  } else {
5140
5220
  log.heading(`Delete plan for ${ws} Library`);
@@ -5160,17 +5240,17 @@ async function deleteCommand(slug, opts = {}, deps = { api }) {
5160
5240
  wouldMutate: true,
5161
5241
  action: "delete",
5162
5242
  message: `${slug} would be deleted from the Library.`,
5163
- next: ["floom delete " + slug + " --yes"]
5243
+ next: [cliCmd(`delete ${slug} --yes`)]
5164
5244
  });
5165
5245
  process.exitCode = opts.exitZero ? 0 : 1;
5166
5246
  return;
5167
5247
  }
5168
5248
  if (!opts.yes) {
5169
5249
  if (!isInteractive()) {
5170
- log.err("floom delete needs confirmation but is not running in an interactive terminal.");
5250
+ log.err(`${cliCmd("delete")} needs confirmation but is not running in an interactive terminal.`);
5171
5251
  log.blank();
5172
5252
  log.info("Re-run with:");
5173
- log.command(`floom delete ${slug} --yes`);
5253
+ log.command(cliCmd(`delete ${slug} --yes`));
5174
5254
  process.exitCode = 2;
5175
5255
  return;
5176
5256
  }
@@ -5199,7 +5279,7 @@ async function deleteCommand(slug, opts = {}, deps = { api }) {
5199
5279
  skill: { slug, version: info.versionSeq, exists: true },
5200
5280
  result: "failed",
5201
5281
  message: error.message,
5202
- next: ["floom delete " + slug + " --yes"]
5282
+ next: [cliCmd(`delete ${slug} --yes`)]
5203
5283
  });
5204
5284
  } else {
5205
5285
  log.err(error.message);
@@ -5215,14 +5295,14 @@ async function deleteCommand(slug, opts = {}, deps = { api }) {
5215
5295
  skill: { slug, version: info.versionSeq, exists: false },
5216
5296
  result: "deleted",
5217
5297
  message: `Deleted ${slug} from the Library.`,
5218
- next: ["floom status"]
5298
+ next: [cliCmd("status")]
5219
5299
  });
5220
5300
  return;
5221
5301
  }
5222
5302
  log.ok(`Deleted ${slug} from the Library.`);
5223
5303
  log.blank();
5224
5304
  log.info("Local copies in your agents still have the skill. Floom will not delete them automatically.");
5225
- printNext([{ command: "floom status", description: "see which local copies are no longer in the Library" }]);
5305
+ printNext([{ command: cliCmd("status"), description: "see which local copies are no longer in the Library" }]);
5226
5306
  }
5227
5307
 
5228
5308
  // src/commands/list.ts
@@ -5236,11 +5316,11 @@ async function listCommand(opts = {}) {
5236
5316
  workspace: { name: "Library", signedIn: false },
5237
5317
  error: { code: "AUTH_REQUIRED", message: "Not signed in." },
5238
5318
  skills: [],
5239
- next: ["floom login"]
5319
+ next: [cliCmd("login")]
5240
5320
  });
5241
5321
  } else {
5242
5322
  log.err("Not signed in.");
5243
- printNext([{ command: "floom login" }]);
5323
+ printNext([{ command: cliCmd("login") }]);
5244
5324
  }
5245
5325
  process.exitCode = 1;
5246
5326
  return;
@@ -5255,7 +5335,7 @@ async function listCommand(opts = {}) {
5255
5335
  workspace: { name: "Library", signedIn: true },
5256
5336
  error: { code: fe?.code ?? "INTERNAL_ERROR", message: e.message },
5257
5337
  skills: [],
5258
- next: ["floom login"]
5338
+ next: [cliCmd("login")]
5259
5339
  });
5260
5340
  process.exitCode = 1;
5261
5341
  return;
@@ -5289,7 +5369,7 @@ async function listCommand(opts = {}) {
5289
5369
  }))
5290
5370
  };
5291
5371
  }),
5292
- next: result.total === 0 ? ["floom push"] : ["floom pull", "floom status"]
5372
+ next: result.total === 0 ? [cliCmd("push")] : [cliCmd("pull"), cliCmd("status")]
5293
5373
  });
5294
5374
  return;
5295
5375
  }
@@ -5304,7 +5384,7 @@ async function listCommand(opts = {}) {
5304
5384
  log.info(` ${tildePath(skill.path)}`);
5305
5385
  }
5306
5386
  }
5307
- printNext([{ command: "floom push", description: "publish your first skills to the Library" }]);
5387
+ printNext([{ command: cliCmd("push"), description: "publish your first skills to the Library" }]);
5308
5388
  return;
5309
5389
  }
5310
5390
  log.heading(`Library \xB7 ${result.total} skill${result.total === 1 ? "" : "s"}`);
@@ -5322,8 +5402,8 @@ async function listCommand(opts = {}) {
5322
5402
  log.info(` ... and ${result.total - rows.length} more`);
5323
5403
  }
5324
5404
  printNext([
5325
- { command: "floom pull", description: "install missing skills into local agents" },
5326
- { command: "floom status", description: "compare every local copy in detail" }
5405
+ { command: cliCmd("pull"), description: "install missing skills into local agents" },
5406
+ { command: cliCmd("status"), description: "compare every local copy in detail" }
5327
5407
  ]);
5328
5408
  }
5329
5409
 
@@ -5388,10 +5468,10 @@ async function pullCommand(skillArg, rawOpts = {}) {
5388
5468
  const planMode = isPlanMode(flags);
5389
5469
  if (!await readAuth()) {
5390
5470
  if (json) {
5391
- emitJson({ workspace: { name: "Library", signedIn: false }, mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], next: ["floom login"] });
5471
+ emitJson({ workspace: { name: "Library", signedIn: false }, mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], next: [cliCmd("login")] });
5392
5472
  } else {
5393
5473
  log.info("Not signed in to Floom.");
5394
- printNext([{ command: "floom login" }]);
5474
+ printNext([{ command: cliCmd("login") }]);
5395
5475
  }
5396
5476
  process.exitCode = 2;
5397
5477
  return;
@@ -5399,14 +5479,14 @@ async function pullCommand(skillArg, rawOpts = {}) {
5399
5479
  const detected = await detectAgents();
5400
5480
  if (detected.length === 0) {
5401
5481
  if (json) {
5402
- emitJson({ workspace: { name: "Library", signedIn: true }, mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], next: ["floom pull --agent claude"] });
5482
+ emitJson({ workspace: { name: "Library", signedIn: true }, mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], next: [cliCmd("pull --agent claude")] });
5403
5483
  } else {
5404
5484
  log.info("No AI agents found on this machine.");
5405
5485
  log.blank();
5406
5486
  log.info("Floom looks for Claude, Codex, Cursor, Gemini, or OpenCode.");
5407
- log.info("Make sure one of these is installed, then run floom pull again.");
5487
+ log.info(`Make sure one of these is installed, then run ${cliCmd("pull")} again.`);
5408
5488
  log.blank();
5409
- log.info("Or use: floom pull --agent claude");
5489
+ log.info(`Or use: ${cliCmd("pull --agent claude")}`);
5410
5490
  log.blank();
5411
5491
  log.info("More help: https://floom.dev/docs#agents");
5412
5492
  }
@@ -5418,19 +5498,19 @@ async function pullCommand(skillArg, rawOpts = {}) {
5418
5498
  if (skillArg) {
5419
5499
  if (flags.allScopes) {
5420
5500
  if (json) {
5421
- 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`] });
5501
+ 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`)] });
5422
5502
  } else {
5423
- log.err("floom pull <skill> targets one copy \u2014 pass --global or --project, not --all-scopes.");
5503
+ log.err(`${cliCmd("pull")} <skill> targets one copy \u2014 pass --global or --project, not --all-scopes.`);
5424
5504
  }
5425
5505
  process.exitCode = 2;
5426
5506
  return;
5427
5507
  }
5428
5508
  if (flags.agents.length !== 1) {
5429
5509
  if (json) {
5430
- emitJson({ workspace: { name: "Library", signedIn: true }, error: "floom pull <skill> needs exactly one --agent", next: [`floom pull ${skillArg} --agent claude`] });
5510
+ emitJson({ workspace: { name: "Library", signedIn: true }, error: `${cliCmd("pull")} <skill> needs exactly one --agent`, next: [cliCmd(`pull ${skillArg} --agent claude`)] });
5431
5511
  } else {
5432
- log.err(`floom pull ${skillArg} needs exactly one --agent <name>.`);
5433
- log.command(`floom pull ${skillArg} --agent claude`);
5512
+ log.err(`${cliCmd("pull")} ${skillArg} needs exactly one --agent <name>.`);
5513
+ log.command(cliCmd(`pull ${skillArg} --agent claude`));
5434
5514
  }
5435
5515
  process.exitCode = 2;
5436
5516
  return;
@@ -5439,7 +5519,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
5439
5519
  const hasProjectSkillsDir = await projectSkillsDirExists(process.cwd());
5440
5520
  if (flags.scope === "project" && !hasProjectSkillsDir) {
5441
5521
  if (json) {
5442
- 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`] });
5522
+ 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`)] });
5443
5523
  } else {
5444
5524
  log.err("No project agent skill directory found. Pass --global or run from a project with agent skills.");
5445
5525
  }
@@ -5448,7 +5528,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
5448
5528
  }
5449
5529
  if (flags.scope === null && hasProjectSkillsDir && !isInteractive()) {
5450
5530
  if (json) {
5451
- 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`] });
5531
+ 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`)] });
5452
5532
  } else {
5453
5533
  log.err("This project has its own agent skill copies. Pass --global or --project.");
5454
5534
  }
@@ -5457,7 +5537,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
5457
5537
  }
5458
5538
  const resolvedScope = flags.scope === "project" ? "project" : "global";
5459
5539
  const dir = resolve5(resolvedScope === "project" ? projectSkillsDir(agent) : globalSkillsDir(agent));
5460
- const scopedPullCommand = `floom pull ${skillArg} --agent ${agent}${resolvedScope === "project" ? " --project" : ""}`;
5540
+ const scopedPullCommand = cliCmd(`pull ${skillArg} --agent ${agent}${resolvedScope === "project" ? " --project" : ""}`);
5461
5541
  if (flags.dryRun || json && !flags.yes) {
5462
5542
  if (json) {
5463
5543
  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`] });
@@ -5473,10 +5553,10 @@ async function pullCommand(skillArg, rawOpts = {}) {
5473
5553
  cleanup.trackDir(join9(dir, ".floom", "tmp"));
5474
5554
  const result = await pullOneSkill(agent, skillArg, { installDir: dir });
5475
5555
  if (json) {
5476
- 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}`] });
5556
+ 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}`)] });
5477
5557
  } else {
5478
5558
  log.ok(`Backed up your local copy and updated ${result.slug} to Library v${result.versionSeq}`);
5479
- printNext([{ command: `floom status --agent ${agent}` }]);
5559
+ printNext([{ command: cliCmd(`status --agent ${agent}`) }]);
5480
5560
  }
5481
5561
  } catch (error) {
5482
5562
  if (json) {
@@ -5490,16 +5570,16 @@ async function pullCommand(skillArg, rawOpts = {}) {
5490
5570
  }
5491
5571
  if (!planMode && flags.agents.length === 0 && !wantAll && !isInteractive()) {
5492
5572
  if (json) {
5493
- emitJson({ workspace: { name: "Library", signedIn: true }, error: "floom pull needs to know which agents to update", next: ["floom pull --all-agents --yes"] });
5573
+ emitJson({ workspace: { name: "Library", signedIn: true }, error: `${cliCmd("pull")} needs to know which agents to update`, next: [cliCmd("pull --all-agents --yes")] });
5494
5574
  } else {
5495
- log.err("floom pull needs to know which agents to update.");
5575
+ log.err(`${cliCmd("pull")} needs to know which agents to update.`);
5496
5576
  log.blank();
5497
5577
  log.info("Pass --agent <name>, repeat --agent, or use --all-agents.");
5498
5578
  log.blank();
5499
5579
  log.info("Examples:");
5500
- log.command("floom pull --agent claude --yes");
5501
- log.command("floom pull --agent claude,codex --yes");
5502
- log.command("floom pull --all-agents --yes");
5580
+ log.command(cliCmd("pull --agent claude --yes"));
5581
+ log.command(cliCmd("pull --agent claude,codex --yes"));
5582
+ log.command(cliCmd("pull --all-agents --yes"));
5503
5583
  }
5504
5584
  process.exitCode = 2;
5505
5585
  return;
@@ -5544,7 +5624,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
5544
5624
  backupPath: null
5545
5625
  }))
5546
5626
  })),
5547
- next: ["floom pull --all-agents --yes"]
5627
+ next: [cliCmd("pull --all-agents --yes")]
5548
5628
  });
5549
5629
  process.exitCode = (wouldMutate || hasSkipped) && !flags.exitZero ? 1 : 0;
5550
5630
  return;
@@ -5582,7 +5662,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
5582
5662
  }
5583
5663
  log.blank();
5584
5664
  const firstConflict = conflicted[0];
5585
- printNext(firstConflict ? [{ command: `floom diff ${firstConflict.slug}`, description: "review the conflict" }] : [{ command: "floom status" }]);
5665
+ printNext(firstConflict ? [{ command: cliCmd(`diff ${firstConflict.slug}`), description: "review the conflict" }] : [{ command: cliCmd("status") }]);
5586
5666
  process.exitCode = flags.exitZero ? 0 : 1;
5587
5667
  return;
5588
5668
  }
@@ -5591,7 +5671,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
5591
5671
  for (const p of plans) {
5592
5672
  log.row([AGENT_LABELS[p.agent], "up to date"], [10]);
5593
5673
  }
5594
- printNext([{ command: "floom status" }]);
5674
+ printNext([{ command: cliCmd("status") }]);
5595
5675
  return;
5596
5676
  }
5597
5677
  log.heading(`Update agent copies from ${workspaceName}`);
@@ -5608,7 +5688,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
5608
5688
  let toUpdate = actionable;
5609
5689
  if (!flags.yes) {
5610
5690
  if (!isInteractive()) {
5611
- log.err("floom pull needs a choice but is not running in an interactive terminal.");
5691
+ log.err(`${cliCmd("pull")} needs a choice but is not running in an interactive terminal.`);
5612
5692
  log.info("Re-run with --agent <name> --yes, or --all-agents --yes.");
5613
5693
  process.exitCode = 2;
5614
5694
  return;
@@ -5668,7 +5748,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
5668
5748
  message: a.message
5669
5749
  }]
5670
5750
  })),
5671
- next: ["floom status"]
5751
+ next: [cliCmd("status")]
5672
5752
  });
5673
5753
  if (failed || hasSkipped) process.exitCode = flags.exitZero ? 0 : 1;
5674
5754
  return;
@@ -5681,8 +5761,8 @@ async function pullCommand(skillArg, rawOpts = {}) {
5681
5761
  }
5682
5762
  }
5683
5763
  log.blank();
5684
- log.info("Backups of any replaced skills are in each agent's .floom/backups/ folder (run floom restore --list to see them).");
5685
- printNext([{ command: "floom status" }]);
5764
+ log.info(`Backups of any replaced skills are in each agent's .floom/backups/ folder (run ${cliCmd("restore --list")} to see them).`);
5765
+ printNext([{ command: cliCmd("status") }]);
5686
5766
  if (failed || hasSkipped) process.exitCode = flags.exitZero ? 0 : 1;
5687
5767
  void skillArg;
5688
5768
  void INSTALL_TARGETS;
@@ -5735,8 +5815,8 @@ function printPreview(plan) {
5735
5815
  log.err(`${count} ${noun} both locally and on the server: Floom won't guess which wins.`);
5736
5816
  for (const skill of plan.conflicts) {
5737
5817
  log.err(` - ${skill.slug} (${skill.version})`);
5738
- log.err(` Keep the server version: floom pull --target ${plan.target}`);
5739
- log.err(` Keep your local version: floom push <${skill.slug}-dir>`);
5818
+ log.err(` Keep the server version: ${cliCmd(`pull --target ${plan.target}`)}`);
5819
+ log.err(` Keep your local version: ${cliCmd(`push <${skill.slug}-dir>`)}`);
5740
5820
  }
5741
5821
  log.err("Your local copy is always backed up to .floom/backups/ first.");
5742
5822
  log.err("More: https://floom.dev/docs#conflicts");
@@ -5862,7 +5942,7 @@ async function runSyncForTarget(options = {}, deps = {}) {
5862
5942
  log.err(` snapshot at: ${failure.snapshotDir}`);
5863
5943
  }
5864
5944
  log.err("");
5865
- log.err(`Re-run \`floom sync --target ${plan.target}\` after the network recovers.`);
5945
+ log.err(`Re-run \`${cliCmd(`sync --target ${plan.target}`)}\` after the network recovers.`);
5866
5946
  process.exitCode = 1;
5867
5947
  return { ok: false, pushFailures, hasConflicts: false };
5868
5948
  }
@@ -5876,6 +5956,13 @@ async function runSyncForTarget(options = {}, deps = {}) {
5876
5956
  init_runtime();
5877
5957
  init_prompt();
5878
5958
  import { resolve as resolve6 } from "node:path";
5959
+ async function runTestSyncDelay(cleanup) {
5960
+ const delayMs = Number.parseInt(process.env.FLOOM_TEST_SYNC_DELAY_MS ?? "", 10);
5961
+ if (!Number.isFinite(delayMs) || delayMs <= 0) return;
5962
+ const trackDir = process.env.FLOOM_TEST_SYNC_TRACK_DIR;
5963
+ if (trackDir) cleanup.trackDir(trackDir);
5964
+ await new Promise((resolve14) => setTimeout(resolve14, delayMs));
5965
+ }
5879
5966
  function appliedFromResult(plan, result) {
5880
5967
  if (result.ok) return { plan, ok: true, message: "synced" };
5881
5968
  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";
@@ -5897,7 +5984,7 @@ function buildApplyJson(workspaceName, applied, opts) {
5897
5984
  ...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" }))
5898
5985
  ]
5899
5986
  })),
5900
- next: ["floom status"]
5987
+ next: [cliCmd("status")]
5901
5988
  };
5902
5989
  }
5903
5990
  function formatAgentLabel(plan) {
@@ -5926,6 +6013,7 @@ async function syncCommand(rawOpts = {}, deps = {}) {
5926
6013
  const readAuthFn = deps.readAuth ?? readAuth;
5927
6014
  const cleanup = installCancellationHandler();
5928
6015
  try {
6016
+ await runTestSyncDelay(cleanup);
5929
6017
  let flags;
5930
6018
  try {
5931
6019
  flags = parseCommonFlags(rawOpts);
@@ -5941,10 +6029,10 @@ async function syncCommand(rawOpts = {}, deps = {}) {
5941
6029
  const planMode = isPlanMode(flags);
5942
6030
  if (!await readAuthFn()) {
5943
6031
  if (json) {
5944
- emitJson({ workspace: { name: "Library", signedIn: false }, mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], next: ["floom login"] });
6032
+ emitJson({ workspace: { name: "Library", signedIn: false }, mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], next: [cliCmd("login")] });
5945
6033
  } else {
5946
6034
  log.info("Not signed in to Floom.");
5947
- printNext([{ command: "floom login" }]);
6035
+ printNext([{ command: cliCmd("login") }]);
5948
6036
  }
5949
6037
  process.exitCode = 2;
5950
6038
  return;
@@ -5952,7 +6040,7 @@ async function syncCommand(rawOpts = {}, deps = {}) {
5952
6040
  const detected = await detectAgentsFn();
5953
6041
  if (detected.length === 0) {
5954
6042
  if (json) {
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"] });
6043
+ 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")] });
5956
6044
  } else {
5957
6045
  log.err("No agents detected \u2014 install Claude, Codex, Cursor, Gemini, or OpenCode, then re-run.");
5958
6046
  log.info("Floom syncs into one of: Claude, Codex, Cursor, Gemini, OpenCode.");
@@ -5965,14 +6053,14 @@ async function syncCommand(rawOpts = {}, deps = {}) {
5965
6053
  const selectedAgents = flags.agents.length > 0 ? flags.agents : detected;
5966
6054
  if (!planMode && !flags.yes && !isInteractive()) {
5967
6055
  if (json) {
5968
- emitJson({ workspace: { name: "Library", signedIn: true }, error: "floom sync needs --yes to apply changes in a non-interactive shell", next: ["floom sync --yes"] });
6056
+ emitJson({ workspace: { name: "Library", signedIn: true }, error: `${cliCmd("sync")} needs --yes to apply changes in a non-interactive shell`, next: [cliCmd("sync --yes")] });
5969
6057
  } else {
5970
- log.err("floom sync needs --yes to apply changes in a non-interactive shell.");
6058
+ log.err(`${cliCmd("sync")} needs --yes to apply changes in a non-interactive shell.`);
5971
6059
  log.blank();
5972
6060
  log.info("Examples:");
5973
- log.command("floom sync --yes # sync every detected agent");
5974
- log.command("floom sync --agent claude --yes");
5975
- log.command("floom sync --agent claude,codex --yes");
6061
+ log.command(cliCmd("sync --yes") + " # sync every detected agent");
6062
+ log.command(cliCmd("sync --agent claude --yes"));
6063
+ log.command(cliCmd("sync --agent claude,codex --yes"));
5976
6064
  }
5977
6065
  process.exitCode = 2;
5978
6066
  return;
@@ -6016,12 +6104,12 @@ async function syncCommand(rawOpts = {}, deps = {}) {
6016
6104
  reason: "changed_in_two_places",
6017
6105
  libraryVersion: 0,
6018
6106
  localPath: p.skillsDir,
6019
- diffCommand: `floom diff ${slug}`,
6020
- pullCommand: `floom pull ${slug} --agent ${p.agent}${p.scope === "project" ? " --project" : ""}`,
6021
- pushCommand: `floom push`
6107
+ diffCommand: cliCmd(`diff ${slug}`),
6108
+ pullCommand: cliCmd(`pull ${slug} --agent ${p.agent}${p.scope === "project" ? " --project" : ""}`),
6109
+ pushCommand: cliCmd("push")
6022
6110
  }))
6023
6111
  })),
6024
- next: ["floom sync --all-agents --yes"]
6112
+ next: [cliCmd("sync --all-agents --yes")]
6025
6113
  });
6026
6114
  process.exitCode = (wouldMutate || hasSkipped) && !flags.exitZero ? 1 : 0;
6027
6115
  return;
@@ -6055,7 +6143,7 @@ async function syncCommand(rawOpts = {}, deps = {}) {
6055
6143
  log.info("Everything is in sync.");
6056
6144
  log.blank();
6057
6145
  for (const p of plans) log.row([AGENT_LABELS[p.agent], "up to date"], [10]);
6058
- printNext([{ command: "floom status" }]);
6146
+ printNext([{ command: cliCmd("status") }]);
6059
6147
  return;
6060
6148
  }
6061
6149
  if (defaultedToAll && detected.length > 1) {
@@ -6074,7 +6162,7 @@ async function syncCommand(rawOpts = {}, deps = {}) {
6074
6162
  let toSync = actionable;
6075
6163
  if (!flags.yes) {
6076
6164
  if (!isInteractive()) {
6077
- log.err("floom sync needs a choice but is not running in an interactive terminal.");
6165
+ log.err(`${cliCmd("sync")} needs a choice but is not running in an interactive terminal.`);
6078
6166
  log.info("Re-run with --agent <name> --yes, or --all-agents --yes.");
6079
6167
  process.exitCode = 2;
6080
6168
  return;
@@ -6124,7 +6212,7 @@ async function syncCommand(rawOpts = {}, deps = {}) {
6124
6212
  log.info(` ${marker} ${formatAgentLabel(entry.plan)} ${entry.message}`);
6125
6213
  }
6126
6214
  log.blank();
6127
- printNext([{ command: "floom status" }]);
6215
+ printNext([{ command: cliCmd("status") }]);
6128
6216
  if (hasSkipped) {
6129
6217
  for (const p of plans) {
6130
6218
  for (const slug of p.conflicts) {
@@ -6148,13 +6236,13 @@ async function syncCommand(rawOpts = {}, deps = {}) {
6148
6236
  if (hasSkipped) {
6149
6237
  const conflictSlug = plans.flatMap((p) => p.conflicts)[0];
6150
6238
  printNext([
6151
- ...conflictSlug ? [{ command: `floom diff ${conflictSlug}`, description: "show what changed before choosing" }] : [],
6152
- { command: "floom status" }
6239
+ ...conflictSlug ? [{ command: cliCmd(`diff ${conflictSlug}`), description: "show what changed before choosing" }] : [],
6240
+ { command: cliCmd("status") }
6153
6241
  ]);
6154
6242
  } else {
6155
6243
  log.blank();
6156
- log.info("Backups of any replaced skills are in each agent's .floom/backups/ folder (run floom restore --list to see them).");
6157
- printNext([{ command: "floom status" }]);
6244
+ log.info(`Backups of any replaced skills are in each agent's .floom/backups/ folder (run ${cliCmd("restore --list")} to see them).`);
6245
+ printNext([{ command: cliCmd("status") }]);
6158
6246
  }
6159
6247
  if (hasSkipped) process.exitCode = 1;
6160
6248
  } finally {
@@ -6233,15 +6321,15 @@ async function statusCommand(rawOpts = {}) {
6233
6321
  const detected = await detectAgents();
6234
6322
  if (detected.length === 0) {
6235
6323
  if (json) {
6236
- emitJson({ workspace: { name: "Library", signedIn: false, librarySkillCount: null }, complete: true, agents: [], skillsNeedingAttention: [], next: ["floom install <link>"] });
6324
+ emitJson({ workspace: { name: "Library", signedIn: false, librarySkillCount: null }, complete: true, agents: [], skillsNeedingAttention: [], next: [cliCmd("install <link>")] });
6237
6325
  } else {
6238
6326
  log.info("No AI agents found on this machine.");
6239
6327
  log.blank();
6240
6328
  log.info("Floom looks for Claude, Codex, Cursor, Gemini, or OpenCode.");
6241
- log.info("Install one of those agents, then run floom status again.");
6329
+ log.info(`Install one of those agents, then run ${cliCmd("status")} again.`);
6242
6330
  log.blank();
6243
6331
  log.info("If someone sent you a shared skill:");
6244
- log.command("floom install <link>");
6332
+ log.command(cliCmd("install <link>"));
6245
6333
  log.blank();
6246
6334
  log.info("More help: https://floom.dev/docs#agents");
6247
6335
  }
@@ -6257,7 +6345,7 @@ async function statusCommand(rawOpts = {}) {
6257
6345
  complete: false,
6258
6346
  agents: detected.map((agent) => ({ name: agent, detected: true, state: null, error: null, summary: emptySummary(), copies: [] })),
6259
6347
  skillsNeedingAttention: [],
6260
- next: ["floom login"]
6348
+ next: [cliCmd("login")]
6261
6349
  });
6262
6350
  process.exitCode = 1;
6263
6351
  return;
@@ -6271,7 +6359,7 @@ async function statusCommand(rawOpts = {}) {
6271
6359
  log.blank();
6272
6360
  log.info("Floom Library");
6273
6361
  log.info(" Not signed in \u2014 can't compare with your team Library.");
6274
- printNext([{ command: "floom login", description: "see and sync your Library" }]);
6362
+ printNext([{ command: cliCmd("login"), description: "see and sync your Library" }]);
6275
6363
  return;
6276
6364
  }
6277
6365
  const me = await fetchMe().catch(() => null);
@@ -6327,7 +6415,7 @@ async function statusCommand(rawOpts = {}) {
6327
6415
  };
6328
6416
  }),
6329
6417
  skillsNeedingAttention: buildAttention(checks),
6330
- next: ["floom sync", "floom status --verbose"]
6418
+ next: [cliCmd("sync"), cliCmd("status --verbose")]
6331
6419
  });
6332
6420
  process.exitCode = complete ? 0 : 1;
6333
6421
  return;
@@ -6343,8 +6431,8 @@ async function statusCommand(rawOpts = {}) {
6343
6431
  }
6344
6432
  }
6345
6433
  printNext([
6346
- { command: "floom push", description: "publish local changes" },
6347
- { command: "floom pull", description: "install Library updates" }
6434
+ { command: cliCmd("push"), description: "publish local changes" },
6435
+ { command: cliCmd("pull"), description: "install Library updates" }
6348
6436
  ]);
6349
6437
  return;
6350
6438
  }
@@ -6353,7 +6441,7 @@ async function statusCommand(rawOpts = {}) {
6353
6441
  if (attention.length === 0 && timedOut.length === 0) {
6354
6442
  log.blank();
6355
6443
  log.info(`All ${skillCount} skill${skillCount === 1 ? "" : "s"} are up to date in ${selectedAgents.map((a) => AGENT_LABELS[a]).join(", ")}.`);
6356
- printNext([{ command: "floom push", description: "if you just created a new skill locally" }]);
6444
+ printNext([{ command: cliCmd("push"), description: "if you just created a new skill locally" }]);
6357
6445
  return;
6358
6446
  }
6359
6447
  log.heading("Skills needing attention");
@@ -6368,7 +6456,7 @@ async function statusCommand(rawOpts = {}) {
6368
6456
  }
6369
6457
  if (attention.length > shown.length) {
6370
6458
  log.blank();
6371
- log.info(`...and ${attention.length - shown.length} more \u2014 run floom status --verbose.`);
6459
+ log.info(`...and ${attention.length - shown.length} more \u2014 run ${cliCmd("status --verbose")}.`);
6372
6460
  }
6373
6461
  const upToDate = checks.flatMap((c) => c.skills).filter((s) => s.state === "up_to_date").length;
6374
6462
  if (upToDate > 0) {
@@ -6379,15 +6467,15 @@ async function statusCommand(rawOpts = {}) {
6379
6467
  if (timedOut.length > 0) {
6380
6468
  log.blank();
6381
6469
  log.info("Status summary");
6382
- log.info(` ${timedOut.length} agent${timedOut.length === 1 ? "" : "s"} timed out \u2014 run floom status --agent ${timedOut[0].agent} to retry.`);
6470
+ log.info(` ${timedOut.length} agent${timedOut.length === 1 ? "" : "s"} timed out \u2014 run ${cliCmd(`status --agent ${timedOut[0].agent}`)} to retry.`);
6383
6471
  }
6384
6472
  const hasLocalUnpublished = attention.some((a) => a.locations.some((l) => l.state === "not_in_library_never_published" || l.state === "local_changes"));
6385
6473
  const hasUpdates = attention.some((a) => a.locations.some((l) => l.state === "update_available"));
6386
6474
  const next = [];
6387
- if (hasLocalUnpublished && hasUpdates) next.push({ command: "floom sync", description: "review local changes and Library updates together" });
6388
- if (hasLocalUnpublished) next.push({ command: "floom push", description: "publish local skills to the Library" });
6389
- if (hasUpdates) next.push({ command: "floom pull", description: "install Library updates" });
6390
- next.push({ command: "floom status --verbose", description: "see every skill" });
6475
+ if (hasLocalUnpublished && hasUpdates) next.push({ command: cliCmd("sync"), description: "review local changes and Library updates together" });
6476
+ if (hasLocalUnpublished) next.push({ command: cliCmd("push"), description: "publish local skills to the Library" });
6477
+ if (hasUpdates) next.push({ command: cliCmd("pull"), description: "install Library updates" });
6478
+ next.push({ command: cliCmd("status --verbose"), description: "see every skill" });
6391
6479
  printNext(next);
6392
6480
  if (timedOut.length > 0) process.exitCode = 1;
6393
6481
  }
@@ -6482,7 +6570,7 @@ var MACHINE_FILE3 = join11(CONFIG_DIR3, "machine.json");
6482
6570
  async function renameDeviceCommand(newLabel) {
6483
6571
  const trimmed = (newLabel ?? "").trim().slice(0, 80);
6484
6572
  if (!trimmed) {
6485
- log.err('Device name cannot be empty. Use: floom rename-device "Work Laptop"');
6573
+ log.err(`Device name cannot be empty. Use: ${cliCmd('rename-device "Work Laptop"')}`);
6486
6574
  process.exitCode = 2;
6487
6575
  return;
6488
6576
  }
@@ -6503,8 +6591,8 @@ async function renameDeviceCommand(newLabel) {
6503
6591
  log.blank();
6504
6592
  log.info("Server sync will happen after your next login.");
6505
6593
  printNext([
6506
- { command: "floom login", description: "sync the device name to your workspace" },
6507
- { command: "floom account", description: "see account details including device name" }
6594
+ { command: cliCmd("login"), description: "sync the device name to your workspace" },
6595
+ { command: cliCmd("account"), description: "see account details including device name" }
6508
6596
  ]);
6509
6597
  return;
6510
6598
  }
@@ -6521,7 +6609,7 @@ async function renameDeviceCommand(newLabel) {
6521
6609
  }
6522
6610
  log.blank();
6523
6611
  log.info("This name appears in your workspace when teammates look at which machine synced last.");
6524
- printNext([{ command: "floom account", description: "see account details including device name" }]);
6612
+ printNext([{ command: cliCmd("account"), description: "see account details including device name" }]);
6525
6613
  }
6526
6614
 
6527
6615
  // src/commands/install.ts
@@ -6698,7 +6786,7 @@ async function installCommand(input, rawOpts = {}) {
6698
6786
  const detected = await detectAgents();
6699
6787
  if (detected.length === 0) {
6700
6788
  if (json) {
6701
- emitJson({ share: shareInfo(shareData), mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], next: ["floom install <link> --agent claude"] });
6789
+ emitJson({ share: shareInfo(shareData), mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], next: [cliCmd("install <link> --agent claude")] });
6702
6790
  } else {
6703
6791
  log.heading("Shared skill");
6704
6792
  log.info(` ${shareData.skill.title} ${shareData.skill.version.display}`);
@@ -6719,16 +6807,16 @@ async function installCommand(input, rawOpts = {}) {
6719
6807
  if (!planMode && selectedAgents.length === 0) {
6720
6808
  if (!isInteractive()) {
6721
6809
  if (json) {
6722
- emitJson({ share: shareInfo(shareData), error: "floom install needs to know where to install", next: ["floom install <link> --all-agents --yes"] });
6810
+ emitJson({ share: shareInfo(shareData), error: `${cliCmd("install")} needs to know where to install`, next: [cliCmd("install <link> --all-agents --yes")] });
6723
6811
  } else {
6724
- log.err("floom install needs to know where to install.");
6812
+ log.err(`${cliCmd("install")} needs to know where to install.`);
6725
6813
  log.blank();
6726
6814
  log.info("Pass --agent <name>, repeat --agent, or use --all-agents.");
6727
6815
  log.blank();
6728
6816
  log.info("Examples:");
6729
- log.command("floom install <link> --agent claude --yes");
6730
- log.command("floom install <link> --agent claude,codex --yes");
6731
- log.command("floom install <link> --all-agents --yes");
6817
+ log.command(cliCmd("install <link> --agent claude --yes"));
6818
+ log.command(cliCmd("install <link> --agent claude,codex --yes"));
6819
+ log.command(cliCmd("install <link> --all-agents --yes"));
6732
6820
  }
6733
6821
  process.exitCode = 2;
6734
6822
  return;
@@ -6797,7 +6885,7 @@ async function installCommand(input, rawOpts = {}) {
6797
6885
  wouldMutate: planAgents.length > 0,
6798
6886
  hasSkipped: false,
6799
6887
  agents: planAgents,
6800
- next: ["floom install <link> --all-agents --yes"]
6888
+ next: [cliCmd("install <link> --all-agents --yes")]
6801
6889
  });
6802
6890
  } else {
6803
6891
  log.heading("Install plan");
@@ -6858,7 +6946,7 @@ async function installCommand(input, rawOpts = {}) {
6858
6946
  backupPath: null,
6859
6947
  message: r.message
6860
6948
  })),
6861
- next: ["floom status"]
6949
+ next: [cliCmd("status")]
6862
6950
  });
6863
6951
  } else {
6864
6952
  printNext([{ command: `Open your agent and ask it to use "${shareData.skill.title}".` }]);
@@ -6999,10 +7087,10 @@ async function newCommand(name, rawOpts = {}) {
6999
7087
  const json = flags.json;
7000
7088
  if (flags.allScopes) {
7001
7089
  if (json) {
7002
- 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`] });
7090
+ 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`)] });
7003
7091
  } else {
7004
- log.err("floom new creates one skill in one location; --all-scopes does not apply.");
7005
- printNext([{ command: `floom new ${name} --agent claude` }]);
7092
+ log.err(`${cliCmd("new")} creates one skill in one location; --all-scopes does not apply.`);
7093
+ printNext([{ command: cliCmd(`new ${name} --agent claude`) }]);
7006
7094
  }
7007
7095
  process.exitCode = 2;
7008
7096
  return;
@@ -7015,7 +7103,7 @@ async function newCommand(name, rawOpts = {}) {
7015
7103
  log.blank();
7016
7104
  log.info("Skill names use lowercase letters, numbers, and hyphens only.");
7017
7105
  log.info("Use:");
7018
- log.command(`floom new ${name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")}`);
7106
+ log.command(cliCmd(`new ${name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")}`));
7019
7107
  }
7020
7108
  process.exitCode = 2;
7021
7109
  return;
@@ -7032,8 +7120,8 @@ async function newCommand(name, rawOpts = {}) {
7032
7120
  log.info(`Floom will not create a nested skill inside ${tildePath(cwd)}.`);
7033
7121
  printNext([
7034
7122
  { command: "cd ~/.claude/skills" },
7035
- { command: `floom new ${name}` },
7036
- { command: `floom new ${name} --agent claude` }
7123
+ { command: cliCmd(`new ${name}`) },
7124
+ { command: cliCmd(`new ${name} --agent claude`) }
7037
7125
  ]);
7038
7126
  }
7039
7127
  process.exitCode = 2;
@@ -7066,7 +7154,7 @@ async function newCommand(name, rawOpts = {}) {
7066
7154
  usesScannedDir = false;
7067
7155
  } else if (!isInteractive() || json) {
7068
7156
  if (json) {
7069
- 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]}`] });
7157
+ 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]}`)] });
7070
7158
  process.exitCode = 2;
7071
7159
  return;
7072
7160
  }
@@ -7075,8 +7163,8 @@ async function newCommand(name, rawOpts = {}) {
7075
7163
  scope = "global";
7076
7164
  usesScannedDir = true;
7077
7165
  } else {
7078
- log.err("floom new needs to know where to create the skill.");
7079
- printNext([{ command: `floom new ${name} --agent ${detected[0]}` }]);
7166
+ log.err(`${cliCmd("new")} needs to know where to create the skill.`);
7167
+ printNext([{ command: cliCmd(`new ${name} --agent ${detected[0]}`) }]);
7080
7168
  process.exitCode = 2;
7081
7169
  return;
7082
7170
  }
@@ -7109,7 +7197,7 @@ async function newCommand(name, rawOpts = {}) {
7109
7197
  }
7110
7198
  if (flags.dryRun || json && !flags.yes) {
7111
7199
  if (json) {
7112
- 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}`] });
7200
+ 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}`)] });
7113
7201
  } else {
7114
7202
  log.heading("Create plan");
7115
7203
  log.blank();
@@ -7130,7 +7218,7 @@ async function newCommand(name, rawOpts = {}) {
7130
7218
  await mkdir7(skillFolder, { recursive: true });
7131
7219
  await writeFile6(skillMd, skillTemplate(title));
7132
7220
  await writeFile6(join13(skillFolder, ".floomignore"), FLOOMIGNORE);
7133
- const pushCmd = usesScannedDir ? "floom push" : `floom push ${tildePath(skillFolder)}`;
7221
+ const pushCmd = usesScannedDir ? cliCmd("push") : cliCmd(`push ${tildePath(skillFolder)}`);
7134
7222
  if (json) {
7135
7223
  emitJson({ skill: { slug: name, name: title }, mode: "apply", applied: true, scope, path: skillFolder, result: "created", reason: null, message: `Created ${skillFolder}`, next: [pushCmd] });
7136
7224
  return;
@@ -7141,7 +7229,7 @@ async function newCommand(name, rawOpts = {}) {
7141
7229
  if (!usesScannedDir) {
7142
7230
  log.blank();
7143
7231
  log.info("No AI agents were detected, so Floom created the skill in this folder.");
7144
- log.info("Zero-arg `floom push` will not find this folder until an agent is installed.");
7232
+ log.info(`Zero-arg \`${cliCmd("push")}\` will not find this folder until an agent is installed.`);
7145
7233
  next.push({ command: `Edit ${tildePath(skillMd)}` });
7146
7234
  } else {
7147
7235
  next.push({ command: `Edit ${tildePath(skillMd)}` });
@@ -7202,20 +7290,20 @@ async function doctorCommand(opts = {}) {
7202
7290
  const auth = await readAuth();
7203
7291
  let authOk = false;
7204
7292
  if (!auth) {
7205
- checks.push({ name: "auth", status: "fail", message: "not signed in", fix: "floom login" });
7293
+ checks.push({ name: "auth", status: "fail", message: "not signed in", fix: cliCmd("login") });
7206
7294
  } else {
7207
7295
  try {
7208
7296
  const me = await fetchMe();
7209
7297
  authOk = true;
7210
7298
  checks.push({ name: "auth", status: "pass", message: `signed in as ${me.user.email}`, fix: null });
7211
7299
  } catch {
7212
- checks.push({ name: "auth", status: "fail", message: "session expired", fix: "floom login" });
7300
+ checks.push({ name: "auth", status: "fail", message: "session expired", fix: cliCmd("login") });
7213
7301
  }
7214
7302
  }
7215
7303
  if (authOk) {
7216
7304
  checks.push({ name: "api", status: "pass", message: "https://floom.dev reachable", fix: null });
7217
7305
  } else {
7218
- checks.push({ name: "api", status: "warn", message: "skipped \u2014 sign in first", fix: "floom login" });
7306
+ checks.push({ name: "api", status: "warn", message: "skipped \u2014 sign in first", fix: cliCmd("login") });
7219
7307
  }
7220
7308
  const agents = await detectAgents();
7221
7309
  if (agents.length === 0) {
@@ -7238,7 +7326,7 @@ async function doctorCommand(opts = {}) {
7238
7326
  }
7239
7327
  }
7240
7328
  if (manifestCorrupt && corruptAgent) {
7241
- const fix = authOk ? `floom sync --dry-run --agent ${corruptAgent}` : "floom login";
7329
+ const fix = authOk ? cliCmd(`sync --dry-run --agent ${corruptAgent}`) : cliCmd("login");
7242
7330
  checks.push({
7243
7331
  name: "manifests",
7244
7332
  status: "fail",
@@ -7273,7 +7361,7 @@ async function doctorCommand(opts = {}) {
7273
7361
  name: "backups",
7274
7362
  status: "warn",
7275
7363
  message: `${formatBytes(backupBytes)} across detected agents`,
7276
- fix: "floom restore --list"
7364
+ fix: cliCmd("restore --list")
7277
7365
  });
7278
7366
  } else {
7279
7367
  checks.push({ name: "backups", status: "pass", message: `${formatBytes(backupBytes)} across ${agents.length} agent${agents.length === 1 ? "" : "s"}`, fix: null });
@@ -7293,7 +7381,7 @@ async function doctorCommand(opts = {}) {
7293
7381
  emitJson({
7294
7382
  ok,
7295
7383
  checks,
7296
- next: failures.length > 0 ? failures.map((c) => c.fix).filter((f) => Boolean(f)) : ["floom status"]
7384
+ next: failures.length > 0 ? failures.map((c) => c.fix).filter((f) => Boolean(f)) : [cliCmd("status")]
7297
7385
  });
7298
7386
  process.exitCode = ok ? 0 : 1;
7299
7387
  return;
@@ -7323,13 +7411,13 @@ async function doctorCommand(opts = {}) {
7323
7411
  if (authFail && manifestCorrupt) {
7324
7412
  log.blank();
7325
7413
  log.info("Fix in this order:");
7326
- log.command("floom login");
7327
- if (corruptAgent) log.command(`floom sync --dry-run --agent ${corruptAgent}`);
7414
+ log.command(cliCmd("login"));
7415
+ if (corruptAgent) log.command(cliCmd(`sync --dry-run --agent ${corruptAgent}`));
7328
7416
  } else {
7329
- printNext(failures.map((c) => ({ command: c.fix ?? "floom status" })));
7417
+ printNext(failures.map((c) => ({ command: c.fix ?? cliCmd("status") })));
7330
7418
  }
7331
7419
  } else {
7332
- printNext([{ command: "floom status" }]);
7420
+ printNext([{ command: cliCmd("status") }]);
7333
7421
  }
7334
7422
  process.exitCode = ok ? 0 : 1;
7335
7423
  }
@@ -7438,7 +7526,7 @@ async function diffCommand(skill, opts = {}) {
7438
7526
  lastSyncedHash: null,
7439
7527
  files: files.map((f) => ({ path: f, status: "added", binary: false }))
7440
7528
  }],
7441
- next: [`floom push ${copy.path}`]
7529
+ next: [cliCmd(`push ${copy.path}`)]
7442
7530
  });
7443
7531
  return;
7444
7532
  }
@@ -7450,7 +7538,7 @@ async function diffCommand(skill, opts = {}) {
7450
7538
  log.blank();
7451
7539
  log.info("Baseline: empty");
7452
7540
  log.info(`Local copy: ${tildePath(copy.path)}`);
7453
- printNext([{ command: `floom push ${tildePath(copy.path)}`, description: `publish ${skill} as v1` }]);
7541
+ printNext([{ command: cliCmd(`push ${tildePath(copy.path)}`), description: `publish ${skill} as v1` }]);
7454
7542
  return;
7455
7543
  }
7456
7544
  const localHash = await skillContentHash(copy.path) ?? "";
@@ -7469,7 +7557,7 @@ async function diffCommand(skill, opts = {}) {
7469
7557
  lastSyncedHash: entry.hash,
7470
7558
  files: []
7471
7559
  }],
7472
- next: ["floom status"]
7560
+ next: [cliCmd("status")]
7473
7561
  });
7474
7562
  return;
7475
7563
  }
@@ -7479,17 +7567,17 @@ async function diffCommand(skill, opts = {}) {
7479
7567
  log.info(`Last synced at v${entry.version_seq}.`);
7480
7568
  log.info(`Local copy: ${tildePath(copy.path)}`);
7481
7569
  log.blank();
7482
- log.info("Run floom status when back online for a full file-by-file comparison.");
7483
- printNext([{ command: "floom status" }]);
7570
+ log.info(`Run ${cliCmd("status")} when back online for a full file-by-file comparison.`);
7571
+ printNext([{ command: cliCmd("status") }]);
7484
7572
  return;
7485
7573
  }
7486
7574
  if (copies.length === 0) {
7487
7575
  if (json) {
7488
- emitJson({ skill, agent: agentArg ?? null, source: "library", library: { available: libraryAvailable, version: libraryVersion, hash: null }, agents: [], next: libraryAvailable ? ["floom pull"] : [] });
7576
+ emitJson({ skill, agent: agentArg ?? null, source: "library", library: { available: libraryAvailable, version: libraryVersion, hash: null }, agents: [], next: libraryAvailable ? [cliCmd("pull")] : [] });
7489
7577
  } else {
7490
7578
  log.info(`No local copy of ${skill} found on this machine.`);
7491
7579
  if (libraryAvailable) {
7492
- printNext([{ command: "floom pull", description: `install ${skill} from the Library` }]);
7580
+ printNext([{ command: cliCmd("pull"), description: `install ${skill} from the Library` }]);
7493
7581
  } else {
7494
7582
  log.err(`${skill} is not in the Library and has no local copy.`);
7495
7583
  }
@@ -7545,7 +7633,7 @@ async function diffCommand(skill, opts = {}) {
7545
7633
  source: libraryAvailable ? "library" : "last_synced_manifest",
7546
7634
  library: { available: libraryAvailable, version: libraryVersion, hash: null },
7547
7635
  agents: agentResults,
7548
- next: ["floom status"]
7636
+ next: [cliCmd("status")]
7549
7637
  });
7550
7638
  return;
7551
7639
  }
@@ -7574,10 +7662,10 @@ async function diffCommand(skill, opts = {}) {
7574
7662
  const conflict = agentResults.find((r) => r.state === "changed_in_two_places");
7575
7663
  const next = [];
7576
7664
  if (conflict) {
7577
- 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)" });
7578
- next.push({ command: `floom push ${tildePath(conflict.path)}`, description: "publish your local version" });
7665
+ 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)" });
7666
+ next.push({ command: cliCmd(`push ${tildePath(conflict.path)}`), description: "publish your local version" });
7579
7667
  } else {
7580
- next.push({ command: "floom status" });
7668
+ next.push({ command: cliCmd("status") });
7581
7669
  }
7582
7670
  printNext(next);
7583
7671
  void sep6;
@@ -7671,7 +7759,7 @@ async function restoreCommand(skill, opts = {}) {
7671
7759
  wouldMutate: false,
7672
7760
  selectedBackup: null,
7673
7761
  message: `${all.length} backups available`,
7674
- next: skill ? [`floom restore ${skill} --agent ${detected[0] ?? "claude"}`] : []
7762
+ next: skill ? [cliCmd(`restore ${skill} --agent ${detected[0] ?? "claude"}`)] : []
7675
7763
  });
7676
7764
  return;
7677
7765
  }
@@ -7693,12 +7781,12 @@ async function restoreCommand(skill, opts = {}) {
7693
7781
  log.info(` ${b.skill.padEnd(16)}${b.timestamp.padEnd(24)}${tildePath(b.path)}`);
7694
7782
  }
7695
7783
  if (all[0]) {
7696
- printNext([{ command: `floom restore ${all[0].skill} --agent ${all[0].agent}` }]);
7784
+ printNext([{ command: cliCmd(`restore ${all[0].skill} --agent ${all[0].agent}`) }]);
7697
7785
  }
7698
7786
  return;
7699
7787
  }
7700
7788
  if (!skill) {
7701
- if (!json) log.err("floom restore needs a skill name, or use floom restore --list to see backups.");
7789
+ if (!json) log.err(`${cliCmd("restore")} needs a skill name, or use ${cliCmd("restore --list")} to see backups.`);
7702
7790
  process.exitCode = 2;
7703
7791
  return;
7704
7792
  }
@@ -7716,10 +7804,10 @@ async function restoreCommand(skill, opts = {}) {
7716
7804
  const backups = (await listBackups(searchAgents)).filter((b) => b.skill === skill);
7717
7805
  if (backups.length === 0) {
7718
7806
  if (json) {
7719
- 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"] });
7807
+ 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")] });
7720
7808
  } else {
7721
7809
  log.err(`No backups found for ${skill}.`);
7722
- printNext([{ command: "floom restore --list" }]);
7810
+ printNext([{ command: cliCmd("restore --list") }]);
7723
7811
  }
7724
7812
  process.exitCode = 2;
7725
7813
  return;
@@ -7744,7 +7832,7 @@ async function restoreCommand(skill, opts = {}) {
7744
7832
  }
7745
7833
  }
7746
7834
  if (opts.yes && !opts.dryRun && !agentArg && !isInteractive()) {
7747
- if (!json) log.err("floom restore needs --agent <name> --backup <timestamp> --yes for non-interactive restore.");
7835
+ if (!json) log.err(`${cliCmd("restore")} needs --agent <name> --backup <timestamp> --yes for non-interactive restore.`);
7748
7836
  process.exitCode = 2;
7749
7837
  return;
7750
7838
  }
@@ -7764,7 +7852,7 @@ async function restoreCommand(skill, opts = {}) {
7764
7852
  wouldMutate: true,
7765
7853
  selectedBackup: selected?.timestamp ?? null,
7766
7854
  message: `${skill} would be restored from ${chosen.timestamp}`,
7767
- next: [`floom restore ${skill} --agent ${agent} --backup ${chosen.timestamp} --yes`]
7855
+ next: [cliCmd(`restore ${skill} --agent ${agent} --backup ${chosen.timestamp} --yes`)]
7768
7856
  });
7769
7857
  } else {
7770
7858
  log.heading("Restore plan");
@@ -7794,8 +7882,8 @@ async function restoreCommand(skill, opts = {}) {
7794
7882
  }
7795
7883
  selected = agentBackups[choice - 1];
7796
7884
  } else {
7797
- log.err(`floom restore needs a backup timestamp. Pass --backup <timestamp>, or run interactively.`);
7798
- log.info("Run floom restore --list to see available backups.");
7885
+ log.err(`${cliCmd("restore")} needs a backup timestamp. Pass --backup <timestamp>, or run interactively.`);
7886
+ log.info(`Run ${cliCmd("restore --list")} to see available backups.`);
7799
7887
  process.exitCode = 2;
7800
7888
  return;
7801
7889
  }
@@ -7839,15 +7927,15 @@ async function restoreCommand(skill, opts = {}) {
7839
7927
  preRestoreBackupPath: preRestoreBackup,
7840
7928
  result: "restored",
7841
7929
  message: `Restored ${skill} from ${selected.timestamp}`,
7842
- next: [`floom status --agent ${agent}`]
7930
+ next: [cliCmd(`status --agent ${agent}`)]
7843
7931
  });
7844
7932
  return;
7845
7933
  }
7846
7934
  log.ok(`Restored ${skill} from ${selected.timestamp}`);
7847
- printNext([{ command: `floom status --agent ${agent}` }]);
7935
+ printNext([{ command: cliCmd(`status --agent ${agent}`) }]);
7848
7936
  } catch (error) {
7849
7937
  if (json) {
7850
- 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"] });
7938
+ 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")] });
7851
7939
  } else {
7852
7940
  log.err(error.message);
7853
7941
  }
@@ -7873,7 +7961,7 @@ async function dashboardCommand() {
7873
7961
  }
7874
7962
  log.blank();
7875
7963
  log.info("Not signed in \u2014 can't compare with your team Library.");
7876
- printNext([{ command: "floom login", description: "sign in to see and sync your Library" }]);
7964
+ printNext([{ command: cliCmd("login"), description: "sign in to see and sync your Library" }]);
7877
7965
  return;
7878
7966
  }
7879
7967
  const me = await fetchMe().catch(() => null);
@@ -7943,17 +8031,17 @@ async function dashboardCommand() {
7943
8031
  }
7944
8032
  const next = [];
7945
8033
  if (anyLocalChanges && anyUpdates) {
7946
- next.push({ command: "floom sync", description: "review local changes and Library updates together" });
7947
- next.push({ command: "floom push", description: "publish local skills to the Library" });
7948
- next.push({ command: "floom pull", description: "update agent copies from the Library" });
8034
+ next.push({ command: cliCmd("sync"), description: "review local changes and Library updates together" });
8035
+ next.push({ command: cliCmd("push"), description: "publish local skills to the Library" });
8036
+ next.push({ command: cliCmd("pull"), description: "update agent copies from the Library" });
7949
8037
  } else if (anyUpdates) {
7950
- next.push({ command: "floom pull", description: "update agent copies from the Library" });
8038
+ next.push({ command: cliCmd("pull"), description: "update agent copies from the Library" });
7951
8039
  } else if (anyLocalChanges) {
7952
- next.push({ command: "floom push", description: "publish local skills to the Library" });
8040
+ next.push({ command: cliCmd("push"), description: "publish local skills to the Library" });
7953
8041
  } else if (!libResult.ok) {
7954
- next.push({ command: "floom status", description: "retry full comparison" });
8042
+ next.push({ command: cliCmd("status"), description: "retry full comparison" });
7955
8043
  }
7956
- next.push({ command: "floom status", description: "see full detail" });
8044
+ next.push({ command: cliCmd("status"), description: "see full detail" });
7957
8045
  printNext(next);
7958
8046
  void [];
7959
8047
  }