@floomhq/floom 3.0.3 → 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.3";
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,7 +3792,7 @@ 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;
@@ -3752,7 +3830,7 @@ async function logoutCommand(opts = {}, deps = {}) {
3752
3830
  );
3753
3831
  log.blank();
3754
3832
  log.info("Your local skill files in ~/.claude/skills, ~/.codex/skills, etc. were not changed.");
3755
- printNext([{ command: "floom login", description: "sign in again" }]);
3833
+ printNext([{ command: cliCmd("login"), description: "sign in again" }]);
3756
3834
  }
3757
3835
 
3758
3836
  // src/commands/account.ts
@@ -3807,18 +3885,18 @@ async function accountCommand(opts = {}) {
3807
3885
  const device = await deviceLabel();
3808
3886
  if (!auth) {
3809
3887
  if (json) {
3810
- 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")] });
3811
3889
  return;
3812
3890
  }
3813
3891
  log.info("Not signed in to Floom.");
3814
- printNext([{ command: "floom login" }]);
3892
+ printNext([{ command: cliCmd("login") }]);
3815
3893
  return;
3816
3894
  }
3817
3895
  if (!json) {
3818
3896
  const rawAuth = await readRawAuth();
3819
3897
  if (rawAuth && !isTrustedApiUrl(rawAuth.apiUrl)) {
3820
3898
  log.warn(
3821
- `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.`
3822
3900
  );
3823
3901
  }
3824
3902
  }
@@ -3830,7 +3908,7 @@ async function accountCommand(opts = {}) {
3830
3908
  email: me.user.email ?? auth.email,
3831
3909
  workspace: me.workspace ? { name: me.workspace.name } : null,
3832
3910
  device: { label: device },
3833
- next: ["floom status", "floom logout"]
3911
+ next: [cliCmd("status"), cliCmd("logout")]
3834
3912
  });
3835
3913
  return;
3836
3914
  }
@@ -3840,19 +3918,19 @@ async function accountCommand(opts = {}) {
3840
3918
  if (me.workspace) log.kv("Workspace:", me.workspace.name);
3841
3919
  log.kv("This machine:", device ?? "not named yet");
3842
3920
  printNext([
3843
- { command: "floom status", description: "see Library and local agent copies" },
3844
- { 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" }
3845
3923
  ]);
3846
3924
  } catch (error) {
3847
3925
  if (json) {
3848
- 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")] });
3849
3927
  process.exitCode = 1;
3850
3928
  return;
3851
3929
  }
3852
3930
  log.err(error.message);
3853
3931
  log.blank();
3854
3932
  log.info("Session expired. Sign in again to continue.");
3855
- printNext([{ command: "floom login" }]);
3933
+ printNext([{ command: cliCmd("login") }]);
3856
3934
  process.exitCode = 1;
3857
3935
  }
3858
3936
  void isJsonMode;
@@ -4720,7 +4798,7 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4720
4798
  syncedCount: 0,
4721
4799
  plan: [],
4722
4800
  error: { code: "AUTH_REQUIRED", message: "Not signed in \u2014 log in first to publish." },
4723
- next: ["floom login"]
4801
+ next: [cliCmd("login")]
4724
4802
  });
4725
4803
  process.exitCode = 1;
4726
4804
  return;
@@ -4730,7 +4808,7 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4730
4808
  log.blank();
4731
4809
  if (skills.length === 0) {
4732
4810
  log.info("No local skill folders found on this machine.");
4733
- printNext([{ command: "floom login" }]);
4811
+ printNext([{ command: cliCmd("login") }]);
4734
4812
  process.exitCode = 1;
4735
4813
  return;
4736
4814
  }
@@ -4738,10 +4816,10 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4738
4816
  for (const skill of skills) log.info(` ${tildePath(skill.path)}`);
4739
4817
  log.blank();
4740
4818
  log.info("To compare and publish them, sign in to Floom.");
4741
- printNext([{ command: "floom login" }]);
4819
+ printNext([{ command: cliCmd("login") }]);
4742
4820
  log.blank();
4743
4821
  log.info("After login, run:");
4744
- log.command("floom push");
4822
+ log.command(cliCmd("push"));
4745
4823
  process.exitCode = 1;
4746
4824
  return;
4747
4825
  }
@@ -4759,7 +4837,7 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4759
4837
  wouldMutate: false,
4760
4838
  syncedCount: 0,
4761
4839
  plan: [],
4762
- next: libCount > 0 ? ["floom pull"] : ["floom new"]
4840
+ next: libCount > 0 ? [cliCmd("pull")] : [cliCmd("new")]
4763
4841
  });
4764
4842
  return;
4765
4843
  }
@@ -4768,11 +4846,11 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4768
4846
  log.blank();
4769
4847
  if (libCount > 0) {
4770
4848
  log.info(`Your Library has ${libCount} skill${libCount === 1 ? "" : "s"}. To install them here, run:`);
4771
- log.command("floom pull");
4849
+ log.command(cliCmd("pull"));
4772
4850
  }
4773
4851
  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" }
4852
+ { command: cliCmd("pull"), description: "install your Library on this machine" },
4853
+ { command: cliCmd("new research-helper"), description: "create a ready-to-edit skill" }
4776
4854
  ]);
4777
4855
  return;
4778
4856
  }
@@ -4815,7 +4893,7 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4815
4893
  lastSyncedLibraryVersion: r.lastSyncedLibraryVersion,
4816
4894
  publishVersion: r.publishVersion
4817
4895
  })),
4818
- next: ["floom push --yes", "floom push"]
4896
+ next: [cliCmd("push --yes"), cliCmd("push")]
4819
4897
  });
4820
4898
  process.exitCode = wouldMutate && !options.exitZero ? 1 : 0;
4821
4899
  return;
@@ -4837,7 +4915,7 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4837
4915
  if (!wouldMutate && refusedRows.length === 0) {
4838
4916
  log.blank();
4839
4917
  log.info("Everything is already in sync with the Library. Nothing to publish.");
4840
- printNext([{ command: "floom status", description: "see every copy" }]);
4918
+ printNext([{ command: cliCmd("status"), description: "see every copy" }]);
4841
4919
  return;
4842
4920
  }
4843
4921
  let toPublish = [];
@@ -4845,12 +4923,12 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4845
4923
  toPublish = safeRows;
4846
4924
  } else if (!isInteractive()) {
4847
4925
  log.blank();
4848
- 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.`);
4849
4927
  log.blank();
4850
4928
  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");
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");
4854
4932
  process.exitCode = 2;
4855
4933
  return;
4856
4934
  } else {
@@ -4900,9 +4978,9 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
4900
4978
  }
4901
4979
  const conflictSlug = skipped.find((r) => r.action === "conflict")?.slug;
4902
4980
  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" }
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" }
4906
4984
  ]);
4907
4985
  if (failures.length > 0 || skipped.length > 0) process.exitCode = 1;
4908
4986
  } finally {
@@ -4915,10 +4993,10 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
4915
4993
  const info = await stat5(folder).catch(() => null);
4916
4994
  if (!info || !info.isDirectory()) {
4917
4995
  if (json) {
4918
- 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")] });
4919
4997
  } else {
4920
4998
  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.");
4999
+ log.info(` Run ${cliCmd("push")} with no arguments to auto-detect your skills, or pass a valid folder path.`);
4922
5000
  }
4923
5001
  process.exitCode = 2;
4924
5002
  return;
@@ -4941,13 +5019,13 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
4941
5019
  syncedCount: 0,
4942
5020
  plan: [],
4943
5021
  error: { code: "AUTH_REQUIRED", message: "Not signed in \u2014 log in first to publish." },
4944
- next: ["floom login"]
5022
+ next: [cliCmd("login")]
4945
5023
  });
4946
5024
  } else {
4947
5025
  log.blank();
4948
5026
  log.err("Not signed in \u2014 log in first to publish.");
4949
5027
  log.info(`Found local skill at ${tildePath(folder)}.`);
4950
- printNext([{ command: "floom login" }]);
5028
+ printNext([{ command: cliCmd("login") }]);
4951
5029
  }
4952
5030
  process.exitCode = 1;
4953
5031
  return;
@@ -4964,7 +5042,7 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
4964
5042
  wouldMutate: false,
4965
5043
  syncedCount: 0,
4966
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 }],
4967
- next: ["floom push --no-secret-check"]
5045
+ next: [cliCmd("push --no-secret-check")]
4968
5046
  });
4969
5047
  process.exitCode = 1;
4970
5048
  return;
@@ -4986,7 +5064,7 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
4986
5064
  wouldMutate: false,
4987
5065
  syncedCount: 0,
4988
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 }],
4989
- next: ["floom push --no-secret-check"]
5067
+ next: [cliCmd("push --no-secret-check")]
4990
5068
  });
4991
5069
  process.exitCode = 1;
4992
5070
  return;
@@ -5011,7 +5089,7 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
5011
5089
  log.info("Nothing published.");
5012
5090
  printNext([
5013
5091
  { command: "Edit .floomignore to exclude secrets or generated files" },
5014
- { command: `floom push ${pathArg}` }
5092
+ { command: cliCmd(`push ${pathArg}`) }
5015
5093
  ]);
5016
5094
  return;
5017
5095
  }
@@ -5026,7 +5104,7 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
5026
5104
  wouldMutate: true,
5027
5105
  syncedCount: 0,
5028
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 }],
5029
- next: ["floom push " + pathArg + " --yes"]
5107
+ next: [cliCmd(`push ${pathArg} --yes`)]
5030
5108
  });
5031
5109
  } else {
5032
5110
  log.heading("Push plan");
@@ -5046,15 +5124,15 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
5046
5124
  mode: "apply",
5047
5125
  applied: true,
5048
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 }],
5049
- next: ["floom pull"]
5127
+ next: [cliCmd("pull")]
5050
5128
  });
5051
5129
  } else {
5052
5130
  log.ok(`Published ${result.skill.slug} ${result.skill.latest.display}`);
5053
- 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}` }]);
5054
5132
  }
5055
5133
  } catch (error) {
5056
5134
  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] });
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}`)] });
5058
5136
  } else {
5059
5137
  log.err(error.message);
5060
5138
  }
@@ -5095,11 +5173,11 @@ async function deleteCommand(slug, opts = {}, deps = { api }) {
5095
5173
  skill: { slug: "", version: null, exists: false },
5096
5174
  wouldMutate: false,
5097
5175
  action: "missing_argument",
5098
- message: "floom delete needs a skill name.",
5099
- next: ["floom list"]
5176
+ message: `${cliCmd("delete")} needs a skill name.`,
5177
+ next: [cliCmd("list")]
5100
5178
  });
5101
5179
  } else {
5102
- 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.`);
5103
5181
  }
5104
5182
  process.exitCode = 2;
5105
5183
  return;
@@ -5116,10 +5194,10 @@ async function deleteCommand(slug, opts = {}, deps = { api }) {
5116
5194
  wouldMutate: false,
5117
5195
  action: "not_found",
5118
5196
  message: `No skill named '${slug}' in ${ws}.`,
5119
- next: ["floom list"]
5197
+ next: [cliCmd("list")]
5120
5198
  });
5121
5199
  } else {
5122
- 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.`);
5123
5201
  }
5124
5202
  process.exitCode = 2;
5125
5203
  return;
@@ -5134,7 +5212,7 @@ async function deleteCommand(slug, opts = {}, deps = { api }) {
5134
5212
  wouldMutate: true,
5135
5213
  action: "delete",
5136
5214
  message: `${slug} would be deleted from the Library.`,
5137
- next: ["floom delete " + slug + " --yes"]
5215
+ next: [cliCmd(`delete ${slug} --yes`)]
5138
5216
  });
5139
5217
  } else {
5140
5218
  log.heading(`Delete plan for ${ws} Library`);
@@ -5160,17 +5238,17 @@ async function deleteCommand(slug, opts = {}, deps = { api }) {
5160
5238
  wouldMutate: true,
5161
5239
  action: "delete",
5162
5240
  message: `${slug} would be deleted from the Library.`,
5163
- next: ["floom delete " + slug + " --yes"]
5241
+ next: [cliCmd(`delete ${slug} --yes`)]
5164
5242
  });
5165
5243
  process.exitCode = opts.exitZero ? 0 : 1;
5166
5244
  return;
5167
5245
  }
5168
5246
  if (!opts.yes) {
5169
5247
  if (!isInteractive()) {
5170
- 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.`);
5171
5249
  log.blank();
5172
5250
  log.info("Re-run with:");
5173
- log.command(`floom delete ${slug} --yes`);
5251
+ log.command(cliCmd(`delete ${slug} --yes`));
5174
5252
  process.exitCode = 2;
5175
5253
  return;
5176
5254
  }
@@ -5199,7 +5277,7 @@ async function deleteCommand(slug, opts = {}, deps = { api }) {
5199
5277
  skill: { slug, version: info.versionSeq, exists: true },
5200
5278
  result: "failed",
5201
5279
  message: error.message,
5202
- next: ["floom delete " + slug + " --yes"]
5280
+ next: [cliCmd(`delete ${slug} --yes`)]
5203
5281
  });
5204
5282
  } else {
5205
5283
  log.err(error.message);
@@ -5215,14 +5293,14 @@ async function deleteCommand(slug, opts = {}, deps = { api }) {
5215
5293
  skill: { slug, version: info.versionSeq, exists: false },
5216
5294
  result: "deleted",
5217
5295
  message: `Deleted ${slug} from the Library.`,
5218
- next: ["floom status"]
5296
+ next: [cliCmd("status")]
5219
5297
  });
5220
5298
  return;
5221
5299
  }
5222
5300
  log.ok(`Deleted ${slug} from the Library.`);
5223
5301
  log.blank();
5224
5302
  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" }]);
5303
+ printNext([{ command: cliCmd("status"), description: "see which local copies are no longer in the Library" }]);
5226
5304
  }
5227
5305
 
5228
5306
  // src/commands/list.ts
@@ -5236,11 +5314,11 @@ async function listCommand(opts = {}) {
5236
5314
  workspace: { name: "Library", signedIn: false },
5237
5315
  error: { code: "AUTH_REQUIRED", message: "Not signed in." },
5238
5316
  skills: [],
5239
- next: ["floom login"]
5317
+ next: [cliCmd("login")]
5240
5318
  });
5241
5319
  } else {
5242
5320
  log.err("Not signed in.");
5243
- printNext([{ command: "floom login" }]);
5321
+ printNext([{ command: cliCmd("login") }]);
5244
5322
  }
5245
5323
  process.exitCode = 1;
5246
5324
  return;
@@ -5255,7 +5333,7 @@ async function listCommand(opts = {}) {
5255
5333
  workspace: { name: "Library", signedIn: true },
5256
5334
  error: { code: fe?.code ?? "INTERNAL_ERROR", message: e.message },
5257
5335
  skills: [],
5258
- next: ["floom login"]
5336
+ next: [cliCmd("login")]
5259
5337
  });
5260
5338
  process.exitCode = 1;
5261
5339
  return;
@@ -5289,7 +5367,7 @@ async function listCommand(opts = {}) {
5289
5367
  }))
5290
5368
  };
5291
5369
  }),
5292
- next: result.total === 0 ? ["floom push"] : ["floom pull", "floom status"]
5370
+ next: result.total === 0 ? [cliCmd("push")] : [cliCmd("pull"), cliCmd("status")]
5293
5371
  });
5294
5372
  return;
5295
5373
  }
@@ -5304,7 +5382,7 @@ async function listCommand(opts = {}) {
5304
5382
  log.info(` ${tildePath(skill.path)}`);
5305
5383
  }
5306
5384
  }
5307
- 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" }]);
5308
5386
  return;
5309
5387
  }
5310
5388
  log.heading(`Library \xB7 ${result.total} skill${result.total === 1 ? "" : "s"}`);
@@ -5322,8 +5400,8 @@ async function listCommand(opts = {}) {
5322
5400
  log.info(` ... and ${result.total - rows.length} more`);
5323
5401
  }
5324
5402
  printNext([
5325
- { command: "floom pull", description: "install missing skills into local agents" },
5326
- { 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" }
5327
5405
  ]);
5328
5406
  }
5329
5407
 
@@ -5388,10 +5466,10 @@ async function pullCommand(skillArg, rawOpts = {}) {
5388
5466
  const planMode = isPlanMode(flags);
5389
5467
  if (!await readAuth()) {
5390
5468
  if (json) {
5391
- 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")] });
5392
5470
  } else {
5393
5471
  log.info("Not signed in to Floom.");
5394
- printNext([{ command: "floom login" }]);
5472
+ printNext([{ command: cliCmd("login") }]);
5395
5473
  }
5396
5474
  process.exitCode = 2;
5397
5475
  return;
@@ -5399,14 +5477,14 @@ async function pullCommand(skillArg, rawOpts = {}) {
5399
5477
  const detected = await detectAgents();
5400
5478
  if (detected.length === 0) {
5401
5479
  if (json) {
5402
- 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")] });
5403
5481
  } else {
5404
5482
  log.info("No AI agents found on this machine.");
5405
5483
  log.blank();
5406
5484
  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.");
5485
+ log.info(`Make sure one of these is installed, then run ${cliCmd("pull")} again.`);
5408
5486
  log.blank();
5409
- log.info("Or use: floom pull --agent claude");
5487
+ log.info(`Or use: ${cliCmd("pull --agent claude")}`);
5410
5488
  log.blank();
5411
5489
  log.info("More help: https://floom.dev/docs#agents");
5412
5490
  }
@@ -5418,19 +5496,19 @@ async function pullCommand(skillArg, rawOpts = {}) {
5418
5496
  if (skillArg) {
5419
5497
  if (flags.allScopes) {
5420
5498
  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`] });
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`)] });
5422
5500
  } else {
5423
- 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.`);
5424
5502
  }
5425
5503
  process.exitCode = 2;
5426
5504
  return;
5427
5505
  }
5428
5506
  if (flags.agents.length !== 1) {
5429
5507
  if (json) {
5430
- 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`)] });
5431
5509
  } else {
5432
- log.err(`floom pull ${skillArg} needs exactly one --agent <name>.`);
5433
- 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`));
5434
5512
  }
5435
5513
  process.exitCode = 2;
5436
5514
  return;
@@ -5439,7 +5517,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
5439
5517
  const hasProjectSkillsDir = await projectSkillsDirExists(process.cwd());
5440
5518
  if (flags.scope === "project" && !hasProjectSkillsDir) {
5441
5519
  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`] });
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`)] });
5443
5521
  } else {
5444
5522
  log.err("No project agent skill directory found. Pass --global or run from a project with agent skills.");
5445
5523
  }
@@ -5448,7 +5526,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
5448
5526
  }
5449
5527
  if (flags.scope === null && hasProjectSkillsDir && !isInteractive()) {
5450
5528
  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`] });
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`)] });
5452
5530
  } else {
5453
5531
  log.err("This project has its own agent skill copies. Pass --global or --project.");
5454
5532
  }
@@ -5457,7 +5535,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
5457
5535
  }
5458
5536
  const resolvedScope = flags.scope === "project" ? "project" : "global";
5459
5537
  const dir = resolve5(resolvedScope === "project" ? projectSkillsDir(agent) : globalSkillsDir(agent));
5460
- const scopedPullCommand = `floom pull ${skillArg} --agent ${agent}${resolvedScope === "project" ? " --project" : ""}`;
5538
+ const scopedPullCommand = cliCmd(`pull ${skillArg} --agent ${agent}${resolvedScope === "project" ? " --project" : ""}`);
5461
5539
  if (flags.dryRun || json && !flags.yes) {
5462
5540
  if (json) {
5463
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`] });
@@ -5473,10 +5551,10 @@ async function pullCommand(skillArg, rawOpts = {}) {
5473
5551
  cleanup.trackDir(join9(dir, ".floom", "tmp"));
5474
5552
  const result = await pullOneSkill(agent, skillArg, { installDir: dir });
5475
5553
  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}`] });
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}`)] });
5477
5555
  } else {
5478
5556
  log.ok(`Backed up your local copy and updated ${result.slug} to Library v${result.versionSeq}`);
5479
- printNext([{ command: `floom status --agent ${agent}` }]);
5557
+ printNext([{ command: cliCmd(`status --agent ${agent}`) }]);
5480
5558
  }
5481
5559
  } catch (error) {
5482
5560
  if (json) {
@@ -5490,16 +5568,16 @@ async function pullCommand(skillArg, rawOpts = {}) {
5490
5568
  }
5491
5569
  if (!planMode && flags.agents.length === 0 && !wantAll && !isInteractive()) {
5492
5570
  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"] });
5571
+ emitJson({ workspace: { name: "Library", signedIn: true }, error: `${cliCmd("pull")} needs to know which agents to update`, next: [cliCmd("pull --all-agents --yes")] });
5494
5572
  } else {
5495
- log.err("floom pull needs to know which agents to update.");
5573
+ log.err(`${cliCmd("pull")} needs to know which agents to update.`);
5496
5574
  log.blank();
5497
5575
  log.info("Pass --agent <name>, repeat --agent, or use --all-agents.");
5498
5576
  log.blank();
5499
5577
  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");
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"));
5503
5581
  }
5504
5582
  process.exitCode = 2;
5505
5583
  return;
@@ -5544,7 +5622,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
5544
5622
  backupPath: null
5545
5623
  }))
5546
5624
  })),
5547
- next: ["floom pull --all-agents --yes"]
5625
+ next: [cliCmd("pull --all-agents --yes")]
5548
5626
  });
5549
5627
  process.exitCode = (wouldMutate || hasSkipped) && !flags.exitZero ? 1 : 0;
5550
5628
  return;
@@ -5582,7 +5660,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
5582
5660
  }
5583
5661
  log.blank();
5584
5662
  const firstConflict = conflicted[0];
5585
- 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") }]);
5586
5664
  process.exitCode = flags.exitZero ? 0 : 1;
5587
5665
  return;
5588
5666
  }
@@ -5591,7 +5669,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
5591
5669
  for (const p of plans) {
5592
5670
  log.row([AGENT_LABELS[p.agent], "up to date"], [10]);
5593
5671
  }
5594
- printNext([{ command: "floom status" }]);
5672
+ printNext([{ command: cliCmd("status") }]);
5595
5673
  return;
5596
5674
  }
5597
5675
  log.heading(`Update agent copies from ${workspaceName}`);
@@ -5608,7 +5686,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
5608
5686
  let toUpdate = actionable;
5609
5687
  if (!flags.yes) {
5610
5688
  if (!isInteractive()) {
5611
- 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.`);
5612
5690
  log.info("Re-run with --agent <name> --yes, or --all-agents --yes.");
5613
5691
  process.exitCode = 2;
5614
5692
  return;
@@ -5668,7 +5746,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
5668
5746
  message: a.message
5669
5747
  }]
5670
5748
  })),
5671
- next: ["floom status"]
5749
+ next: [cliCmd("status")]
5672
5750
  });
5673
5751
  if (failed || hasSkipped) process.exitCode = flags.exitZero ? 0 : 1;
5674
5752
  return;
@@ -5681,8 +5759,8 @@ async function pullCommand(skillArg, rawOpts = {}) {
5681
5759
  }
5682
5760
  }
5683
5761
  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" }]);
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") }]);
5686
5764
  if (failed || hasSkipped) process.exitCode = flags.exitZero ? 0 : 1;
5687
5765
  void skillArg;
5688
5766
  void INSTALL_TARGETS;
@@ -5735,8 +5813,8 @@ function printPreview(plan) {
5735
5813
  log.err(`${count} ${noun} both locally and on the server: Floom won't guess which wins.`);
5736
5814
  for (const skill of plan.conflicts) {
5737
5815
  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>`);
5816
+ log.err(` Keep the server version: ${cliCmd(`pull --target ${plan.target}`)}`);
5817
+ log.err(` Keep your local version: ${cliCmd(`push <${skill.slug}-dir>`)}`);
5740
5818
  }
5741
5819
  log.err("Your local copy is always backed up to .floom/backups/ first.");
5742
5820
  log.err("More: https://floom.dev/docs#conflicts");
@@ -5862,7 +5940,7 @@ async function runSyncForTarget(options = {}, deps = {}) {
5862
5940
  log.err(` snapshot at: ${failure.snapshotDir}`);
5863
5941
  }
5864
5942
  log.err("");
5865
- 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.`);
5866
5944
  process.exitCode = 1;
5867
5945
  return { ok: false, pushFailures, hasConflicts: false };
5868
5946
  }
@@ -5876,6 +5954,13 @@ async function runSyncForTarget(options = {}, deps = {}) {
5876
5954
  init_runtime();
5877
5955
  init_prompt();
5878
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
+ }
5879
5964
  function appliedFromResult(plan, result) {
5880
5965
  if (result.ok) return { plan, ok: true, message: "synced" };
5881
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";
@@ -5897,7 +5982,7 @@ function buildApplyJson(workspaceName, applied, opts) {
5897
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" }))
5898
5983
  ]
5899
5984
  })),
5900
- next: ["floom status"]
5985
+ next: [cliCmd("status")]
5901
5986
  };
5902
5987
  }
5903
5988
  function formatAgentLabel(plan) {
@@ -5926,6 +6011,7 @@ async function syncCommand(rawOpts = {}, deps = {}) {
5926
6011
  const readAuthFn = deps.readAuth ?? readAuth;
5927
6012
  const cleanup = installCancellationHandler();
5928
6013
  try {
6014
+ await runTestSyncDelay(cleanup);
5929
6015
  let flags;
5930
6016
  try {
5931
6017
  flags = parseCommonFlags(rawOpts);
@@ -5941,10 +6027,10 @@ async function syncCommand(rawOpts = {}, deps = {}) {
5941
6027
  const planMode = isPlanMode(flags);
5942
6028
  if (!await readAuthFn()) {
5943
6029
  if (json) {
5944
- 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")] });
5945
6031
  } else {
5946
6032
  log.info("Not signed in to Floom.");
5947
- printNext([{ command: "floom login" }]);
6033
+ printNext([{ command: cliCmd("login") }]);
5948
6034
  }
5949
6035
  process.exitCode = 2;
5950
6036
  return;
@@ -5952,7 +6038,7 @@ async function syncCommand(rawOpts = {}, deps = {}) {
5952
6038
  const detected = await detectAgentsFn();
5953
6039
  if (detected.length === 0) {
5954
6040
  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"] });
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")] });
5956
6042
  } else {
5957
6043
  log.err("No agents detected \u2014 install Claude, Codex, Cursor, Gemini, or OpenCode, then re-run.");
5958
6044
  log.info("Floom syncs into one of: Claude, Codex, Cursor, Gemini, OpenCode.");
@@ -5965,14 +6051,14 @@ async function syncCommand(rawOpts = {}, deps = {}) {
5965
6051
  const selectedAgents = flags.agents.length > 0 ? flags.agents : detected;
5966
6052
  if (!planMode && !flags.yes && !isInteractive()) {
5967
6053
  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"] });
6054
+ emitJson({ workspace: { name: "Library", signedIn: true }, error: `${cliCmd("sync")} needs --yes to apply changes in a non-interactive shell`, next: [cliCmd("sync --yes")] });
5969
6055
  } else {
5970
- 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.`);
5971
6057
  log.blank();
5972
6058
  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");
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"));
5976
6062
  }
5977
6063
  process.exitCode = 2;
5978
6064
  return;
@@ -6016,12 +6102,12 @@ async function syncCommand(rawOpts = {}, deps = {}) {
6016
6102
  reason: "changed_in_two_places",
6017
6103
  libraryVersion: 0,
6018
6104
  localPath: p.skillsDir,
6019
- diffCommand: `floom diff ${slug}`,
6020
- pullCommand: `floom pull ${slug} --agent ${p.agent}${p.scope === "project" ? " --project" : ""}`,
6021
- pushCommand: `floom push`
6105
+ diffCommand: cliCmd(`diff ${slug}`),
6106
+ pullCommand: cliCmd(`pull ${slug} --agent ${p.agent}${p.scope === "project" ? " --project" : ""}`),
6107
+ pushCommand: cliCmd("push")
6022
6108
  }))
6023
6109
  })),
6024
- next: ["floom sync --all-agents --yes"]
6110
+ next: [cliCmd("sync --all-agents --yes")]
6025
6111
  });
6026
6112
  process.exitCode = (wouldMutate || hasSkipped) && !flags.exitZero ? 1 : 0;
6027
6113
  return;
@@ -6055,7 +6141,7 @@ async function syncCommand(rawOpts = {}, deps = {}) {
6055
6141
  log.info("Everything is in sync.");
6056
6142
  log.blank();
6057
6143
  for (const p of plans) log.row([AGENT_LABELS[p.agent], "up to date"], [10]);
6058
- printNext([{ command: "floom status" }]);
6144
+ printNext([{ command: cliCmd("status") }]);
6059
6145
  return;
6060
6146
  }
6061
6147
  if (defaultedToAll && detected.length > 1) {
@@ -6074,7 +6160,7 @@ async function syncCommand(rawOpts = {}, deps = {}) {
6074
6160
  let toSync = actionable;
6075
6161
  if (!flags.yes) {
6076
6162
  if (!isInteractive()) {
6077
- 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.`);
6078
6164
  log.info("Re-run with --agent <name> --yes, or --all-agents --yes.");
6079
6165
  process.exitCode = 2;
6080
6166
  return;
@@ -6124,7 +6210,7 @@ async function syncCommand(rawOpts = {}, deps = {}) {
6124
6210
  log.info(` ${marker} ${formatAgentLabel(entry.plan)} ${entry.message}`);
6125
6211
  }
6126
6212
  log.blank();
6127
- printNext([{ command: "floom status" }]);
6213
+ printNext([{ command: cliCmd("status") }]);
6128
6214
  if (hasSkipped) {
6129
6215
  for (const p of plans) {
6130
6216
  for (const slug of p.conflicts) {
@@ -6148,13 +6234,13 @@ async function syncCommand(rawOpts = {}, deps = {}) {
6148
6234
  if (hasSkipped) {
6149
6235
  const conflictSlug = plans.flatMap((p) => p.conflicts)[0];
6150
6236
  printNext([
6151
- ...conflictSlug ? [{ command: `floom diff ${conflictSlug}`, description: "show what changed before choosing" }] : [],
6152
- { command: "floom status" }
6237
+ ...conflictSlug ? [{ command: cliCmd(`diff ${conflictSlug}`), description: "show what changed before choosing" }] : [],
6238
+ { command: cliCmd("status") }
6153
6239
  ]);
6154
6240
  } else {
6155
6241
  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" }]);
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") }]);
6158
6244
  }
6159
6245
  if (hasSkipped) process.exitCode = 1;
6160
6246
  } finally {
@@ -6233,15 +6319,15 @@ async function statusCommand(rawOpts = {}) {
6233
6319
  const detected = await detectAgents();
6234
6320
  if (detected.length === 0) {
6235
6321
  if (json) {
6236
- 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>")] });
6237
6323
  } else {
6238
6324
  log.info("No AI agents found on this machine.");
6239
6325
  log.blank();
6240
6326
  log.info("Floom looks for Claude, Codex, Cursor, Gemini, or OpenCode.");
6241
- 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.`);
6242
6328
  log.blank();
6243
6329
  log.info("If someone sent you a shared skill:");
6244
- log.command("floom install <link>");
6330
+ log.command(cliCmd("install <link>"));
6245
6331
  log.blank();
6246
6332
  log.info("More help: https://floom.dev/docs#agents");
6247
6333
  }
@@ -6257,7 +6343,7 @@ async function statusCommand(rawOpts = {}) {
6257
6343
  complete: false,
6258
6344
  agents: detected.map((agent) => ({ name: agent, detected: true, state: null, error: null, summary: emptySummary(), copies: [] })),
6259
6345
  skillsNeedingAttention: [],
6260
- next: ["floom login"]
6346
+ next: [cliCmd("login")]
6261
6347
  });
6262
6348
  process.exitCode = 1;
6263
6349
  return;
@@ -6271,7 +6357,7 @@ async function statusCommand(rawOpts = {}) {
6271
6357
  log.blank();
6272
6358
  log.info("Floom Library");
6273
6359
  log.info(" Not signed in \u2014 can't compare with your team Library.");
6274
- printNext([{ command: "floom login", description: "see and sync your Library" }]);
6360
+ printNext([{ command: cliCmd("login"), description: "see and sync your Library" }]);
6275
6361
  return;
6276
6362
  }
6277
6363
  const me = await fetchMe().catch(() => null);
@@ -6327,7 +6413,7 @@ async function statusCommand(rawOpts = {}) {
6327
6413
  };
6328
6414
  }),
6329
6415
  skillsNeedingAttention: buildAttention(checks),
6330
- next: ["floom sync", "floom status --verbose"]
6416
+ next: [cliCmd("sync"), cliCmd("status --verbose")]
6331
6417
  });
6332
6418
  process.exitCode = complete ? 0 : 1;
6333
6419
  return;
@@ -6343,8 +6429,8 @@ async function statusCommand(rawOpts = {}) {
6343
6429
  }
6344
6430
  }
6345
6431
  printNext([
6346
- { command: "floom push", description: "publish local changes" },
6347
- { command: "floom pull", description: "install Library updates" }
6432
+ { command: cliCmd("push"), description: "publish local changes" },
6433
+ { command: cliCmd("pull"), description: "install Library updates" }
6348
6434
  ]);
6349
6435
  return;
6350
6436
  }
@@ -6353,7 +6439,7 @@ async function statusCommand(rawOpts = {}) {
6353
6439
  if (attention.length === 0 && timedOut.length === 0) {
6354
6440
  log.blank();
6355
6441
  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" }]);
6442
+ printNext([{ command: cliCmd("push"), description: "if you just created a new skill locally" }]);
6357
6443
  return;
6358
6444
  }
6359
6445
  log.heading("Skills needing attention");
@@ -6368,7 +6454,7 @@ async function statusCommand(rawOpts = {}) {
6368
6454
  }
6369
6455
  if (attention.length > shown.length) {
6370
6456
  log.blank();
6371
- 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")}.`);
6372
6458
  }
6373
6459
  const upToDate = checks.flatMap((c) => c.skills).filter((s) => s.state === "up_to_date").length;
6374
6460
  if (upToDate > 0) {
@@ -6379,15 +6465,15 @@ async function statusCommand(rawOpts = {}) {
6379
6465
  if (timedOut.length > 0) {
6380
6466
  log.blank();
6381
6467
  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.`);
6468
+ log.info(` ${timedOut.length} agent${timedOut.length === 1 ? "" : "s"} timed out \u2014 run ${cliCmd(`status --agent ${timedOut[0].agent}`)} to retry.`);
6383
6469
  }
6384
6470
  const hasLocalUnpublished = attention.some((a) => a.locations.some((l) => l.state === "not_in_library_never_published" || l.state === "local_changes"));
6385
6471
  const hasUpdates = attention.some((a) => a.locations.some((l) => l.state === "update_available"));
6386
6472
  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" });
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" });
6391
6477
  printNext(next);
6392
6478
  if (timedOut.length > 0) process.exitCode = 1;
6393
6479
  }
@@ -6482,7 +6568,7 @@ var MACHINE_FILE3 = join11(CONFIG_DIR3, "machine.json");
6482
6568
  async function renameDeviceCommand(newLabel) {
6483
6569
  const trimmed = (newLabel ?? "").trim().slice(0, 80);
6484
6570
  if (!trimmed) {
6485
- 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"')}`);
6486
6572
  process.exitCode = 2;
6487
6573
  return;
6488
6574
  }
@@ -6503,8 +6589,8 @@ async function renameDeviceCommand(newLabel) {
6503
6589
  log.blank();
6504
6590
  log.info("Server sync will happen after your next login.");
6505
6591
  printNext([
6506
- { command: "floom login", description: "sync the device name to your workspace" },
6507
- { 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" }
6508
6594
  ]);
6509
6595
  return;
6510
6596
  }
@@ -6521,7 +6607,7 @@ async function renameDeviceCommand(newLabel) {
6521
6607
  }
6522
6608
  log.blank();
6523
6609
  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" }]);
6610
+ printNext([{ command: cliCmd("account"), description: "see account details including device name" }]);
6525
6611
  }
6526
6612
 
6527
6613
  // src/commands/install.ts
@@ -6698,7 +6784,7 @@ async function installCommand(input, rawOpts = {}) {
6698
6784
  const detected = await detectAgents();
6699
6785
  if (detected.length === 0) {
6700
6786
  if (json) {
6701
- 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")] });
6702
6788
  } else {
6703
6789
  log.heading("Shared skill");
6704
6790
  log.info(` ${shareData.skill.title} ${shareData.skill.version.display}`);
@@ -6719,16 +6805,16 @@ async function installCommand(input, rawOpts = {}) {
6719
6805
  if (!planMode && selectedAgents.length === 0) {
6720
6806
  if (!isInteractive()) {
6721
6807
  if (json) {
6722
- 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")] });
6723
6809
  } else {
6724
- log.err("floom install needs to know where to install.");
6810
+ log.err(`${cliCmd("install")} needs to know where to install.`);
6725
6811
  log.blank();
6726
6812
  log.info("Pass --agent <name>, repeat --agent, or use --all-agents.");
6727
6813
  log.blank();
6728
6814
  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");
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"));
6732
6818
  }
6733
6819
  process.exitCode = 2;
6734
6820
  return;
@@ -6797,7 +6883,7 @@ async function installCommand(input, rawOpts = {}) {
6797
6883
  wouldMutate: planAgents.length > 0,
6798
6884
  hasSkipped: false,
6799
6885
  agents: planAgents,
6800
- next: ["floom install <link> --all-agents --yes"]
6886
+ next: [cliCmd("install <link> --all-agents --yes")]
6801
6887
  });
6802
6888
  } else {
6803
6889
  log.heading("Install plan");
@@ -6858,7 +6944,7 @@ async function installCommand(input, rawOpts = {}) {
6858
6944
  backupPath: null,
6859
6945
  message: r.message
6860
6946
  })),
6861
- next: ["floom status"]
6947
+ next: [cliCmd("status")]
6862
6948
  });
6863
6949
  } else {
6864
6950
  printNext([{ command: `Open your agent and ask it to use "${shareData.skill.title}".` }]);
@@ -6999,10 +7085,10 @@ async function newCommand(name, rawOpts = {}) {
6999
7085
  const json = flags.json;
7000
7086
  if (flags.allScopes) {
7001
7087
  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`] });
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`)] });
7003
7089
  } 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` }]);
7090
+ log.err(`${cliCmd("new")} creates one skill in one location; --all-scopes does not apply.`);
7091
+ printNext([{ command: cliCmd(`new ${name} --agent claude`) }]);
7006
7092
  }
7007
7093
  process.exitCode = 2;
7008
7094
  return;
@@ -7015,7 +7101,7 @@ async function newCommand(name, rawOpts = {}) {
7015
7101
  log.blank();
7016
7102
  log.info("Skill names use lowercase letters, numbers, and hyphens only.");
7017
7103
  log.info("Use:");
7018
- 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, "")}`));
7019
7105
  }
7020
7106
  process.exitCode = 2;
7021
7107
  return;
@@ -7032,8 +7118,8 @@ async function newCommand(name, rawOpts = {}) {
7032
7118
  log.info(`Floom will not create a nested skill inside ${tildePath(cwd)}.`);
7033
7119
  printNext([
7034
7120
  { command: "cd ~/.claude/skills" },
7035
- { command: `floom new ${name}` },
7036
- { command: `floom new ${name} --agent claude` }
7121
+ { command: cliCmd(`new ${name}`) },
7122
+ { command: cliCmd(`new ${name} --agent claude`) }
7037
7123
  ]);
7038
7124
  }
7039
7125
  process.exitCode = 2;
@@ -7066,7 +7152,7 @@ async function newCommand(name, rawOpts = {}) {
7066
7152
  usesScannedDir = false;
7067
7153
  } else if (!isInteractive() || json) {
7068
7154
  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]}`] });
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]}`)] });
7070
7156
  process.exitCode = 2;
7071
7157
  return;
7072
7158
  }
@@ -7075,8 +7161,8 @@ async function newCommand(name, rawOpts = {}) {
7075
7161
  scope = "global";
7076
7162
  usesScannedDir = true;
7077
7163
  } else {
7078
- log.err("floom new needs to know where to create the skill.");
7079
- 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]}`) }]);
7080
7166
  process.exitCode = 2;
7081
7167
  return;
7082
7168
  }
@@ -7109,7 +7195,7 @@ async function newCommand(name, rawOpts = {}) {
7109
7195
  }
7110
7196
  if (flags.dryRun || json && !flags.yes) {
7111
7197
  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}`] });
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}`)] });
7113
7199
  } else {
7114
7200
  log.heading("Create plan");
7115
7201
  log.blank();
@@ -7130,7 +7216,7 @@ async function newCommand(name, rawOpts = {}) {
7130
7216
  await mkdir7(skillFolder, { recursive: true });
7131
7217
  await writeFile6(skillMd, skillTemplate(title));
7132
7218
  await writeFile6(join13(skillFolder, ".floomignore"), FLOOMIGNORE);
7133
- const pushCmd = usesScannedDir ? "floom push" : `floom push ${tildePath(skillFolder)}`;
7219
+ const pushCmd = usesScannedDir ? cliCmd("push") : cliCmd(`push ${tildePath(skillFolder)}`);
7134
7220
  if (json) {
7135
7221
  emitJson({ skill: { slug: name, name: title }, mode: "apply", applied: true, scope, path: skillFolder, result: "created", reason: null, message: `Created ${skillFolder}`, next: [pushCmd] });
7136
7222
  return;
@@ -7141,7 +7227,7 @@ async function newCommand(name, rawOpts = {}) {
7141
7227
  if (!usesScannedDir) {
7142
7228
  log.blank();
7143
7229
  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.");
7230
+ log.info(`Zero-arg \`${cliCmd("push")}\` will not find this folder until an agent is installed.`);
7145
7231
  next.push({ command: `Edit ${tildePath(skillMd)}` });
7146
7232
  } else {
7147
7233
  next.push({ command: `Edit ${tildePath(skillMd)}` });
@@ -7202,20 +7288,20 @@ async function doctorCommand(opts = {}) {
7202
7288
  const auth = await readAuth();
7203
7289
  let authOk = false;
7204
7290
  if (!auth) {
7205
- 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") });
7206
7292
  } else {
7207
7293
  try {
7208
7294
  const me = await fetchMe();
7209
7295
  authOk = true;
7210
7296
  checks.push({ name: "auth", status: "pass", message: `signed in as ${me.user.email}`, fix: null });
7211
7297
  } catch {
7212
- 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") });
7213
7299
  }
7214
7300
  }
7215
7301
  if (authOk) {
7216
7302
  checks.push({ name: "api", status: "pass", message: "https://floom.dev reachable", fix: null });
7217
7303
  } else {
7218
- 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") });
7219
7305
  }
7220
7306
  const agents = await detectAgents();
7221
7307
  if (agents.length === 0) {
@@ -7238,7 +7324,7 @@ async function doctorCommand(opts = {}) {
7238
7324
  }
7239
7325
  }
7240
7326
  if (manifestCorrupt && corruptAgent) {
7241
- const fix = authOk ? `floom sync --dry-run --agent ${corruptAgent}` : "floom login";
7327
+ const fix = authOk ? cliCmd(`sync --dry-run --agent ${corruptAgent}`) : cliCmd("login");
7242
7328
  checks.push({
7243
7329
  name: "manifests",
7244
7330
  status: "fail",
@@ -7273,7 +7359,7 @@ async function doctorCommand(opts = {}) {
7273
7359
  name: "backups",
7274
7360
  status: "warn",
7275
7361
  message: `${formatBytes(backupBytes)} across detected agents`,
7276
- fix: "floom restore --list"
7362
+ fix: cliCmd("restore --list")
7277
7363
  });
7278
7364
  } else {
7279
7365
  checks.push({ name: "backups", status: "pass", message: `${formatBytes(backupBytes)} across ${agents.length} agent${agents.length === 1 ? "" : "s"}`, fix: null });
@@ -7293,7 +7379,7 @@ async function doctorCommand(opts = {}) {
7293
7379
  emitJson({
7294
7380
  ok,
7295
7381
  checks,
7296
- 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")]
7297
7383
  });
7298
7384
  process.exitCode = ok ? 0 : 1;
7299
7385
  return;
@@ -7323,13 +7409,13 @@ async function doctorCommand(opts = {}) {
7323
7409
  if (authFail && manifestCorrupt) {
7324
7410
  log.blank();
7325
7411
  log.info("Fix in this order:");
7326
- log.command("floom login");
7327
- 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}`));
7328
7414
  } else {
7329
- printNext(failures.map((c) => ({ command: c.fix ?? "floom status" })));
7415
+ printNext(failures.map((c) => ({ command: c.fix ?? cliCmd("status") })));
7330
7416
  }
7331
7417
  } else {
7332
- printNext([{ command: "floom status" }]);
7418
+ printNext([{ command: cliCmd("status") }]);
7333
7419
  }
7334
7420
  process.exitCode = ok ? 0 : 1;
7335
7421
  }
@@ -7438,7 +7524,7 @@ async function diffCommand(skill, opts = {}) {
7438
7524
  lastSyncedHash: null,
7439
7525
  files: files.map((f) => ({ path: f, status: "added", binary: false }))
7440
7526
  }],
7441
- next: [`floom push ${copy.path}`]
7527
+ next: [cliCmd(`push ${copy.path}`)]
7442
7528
  });
7443
7529
  return;
7444
7530
  }
@@ -7450,7 +7536,7 @@ async function diffCommand(skill, opts = {}) {
7450
7536
  log.blank();
7451
7537
  log.info("Baseline: empty");
7452
7538
  log.info(`Local copy: ${tildePath(copy.path)}`);
7453
- 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` }]);
7454
7540
  return;
7455
7541
  }
7456
7542
  const localHash = await skillContentHash(copy.path) ?? "";
@@ -7469,7 +7555,7 @@ async function diffCommand(skill, opts = {}) {
7469
7555
  lastSyncedHash: entry.hash,
7470
7556
  files: []
7471
7557
  }],
7472
- next: ["floom status"]
7558
+ next: [cliCmd("status")]
7473
7559
  });
7474
7560
  return;
7475
7561
  }
@@ -7479,17 +7565,17 @@ async function diffCommand(skill, opts = {}) {
7479
7565
  log.info(`Last synced at v${entry.version_seq}.`);
7480
7566
  log.info(`Local copy: ${tildePath(copy.path)}`);
7481
7567
  log.blank();
7482
- log.info("Run floom status when back online for a full file-by-file comparison.");
7483
- 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") }]);
7484
7570
  return;
7485
7571
  }
7486
7572
  if (copies.length === 0) {
7487
7573
  if (json) {
7488
- 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")] : [] });
7489
7575
  } else {
7490
7576
  log.info(`No local copy of ${skill} found on this machine.`);
7491
7577
  if (libraryAvailable) {
7492
- printNext([{ command: "floom pull", description: `install ${skill} from the Library` }]);
7578
+ printNext([{ command: cliCmd("pull"), description: `install ${skill} from the Library` }]);
7493
7579
  } else {
7494
7580
  log.err(`${skill} is not in the Library and has no local copy.`);
7495
7581
  }
@@ -7545,7 +7631,7 @@ async function diffCommand(skill, opts = {}) {
7545
7631
  source: libraryAvailable ? "library" : "last_synced_manifest",
7546
7632
  library: { available: libraryAvailable, version: libraryVersion, hash: null },
7547
7633
  agents: agentResults,
7548
- next: ["floom status"]
7634
+ next: [cliCmd("status")]
7549
7635
  });
7550
7636
  return;
7551
7637
  }
@@ -7574,10 +7660,10 @@ async function diffCommand(skill, opts = {}) {
7574
7660
  const conflict = agentResults.find((r) => r.state === "changed_in_two_places");
7575
7661
  const next = [];
7576
7662
  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" });
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" });
7579
7665
  } else {
7580
- next.push({ command: "floom status" });
7666
+ next.push({ command: cliCmd("status") });
7581
7667
  }
7582
7668
  printNext(next);
7583
7669
  void sep6;
@@ -7671,7 +7757,7 @@ async function restoreCommand(skill, opts = {}) {
7671
7757
  wouldMutate: false,
7672
7758
  selectedBackup: null,
7673
7759
  message: `${all.length} backups available`,
7674
- next: skill ? [`floom restore ${skill} --agent ${detected[0] ?? "claude"}`] : []
7760
+ next: skill ? [cliCmd(`restore ${skill} --agent ${detected[0] ?? "claude"}`)] : []
7675
7761
  });
7676
7762
  return;
7677
7763
  }
@@ -7693,12 +7779,12 @@ async function restoreCommand(skill, opts = {}) {
7693
7779
  log.info(` ${b.skill.padEnd(16)}${b.timestamp.padEnd(24)}${tildePath(b.path)}`);
7694
7780
  }
7695
7781
  if (all[0]) {
7696
- printNext([{ command: `floom restore ${all[0].skill} --agent ${all[0].agent}` }]);
7782
+ printNext([{ command: cliCmd(`restore ${all[0].skill} --agent ${all[0].agent}`) }]);
7697
7783
  }
7698
7784
  return;
7699
7785
  }
7700
7786
  if (!skill) {
7701
- 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.`);
7702
7788
  process.exitCode = 2;
7703
7789
  return;
7704
7790
  }
@@ -7716,10 +7802,10 @@ async function restoreCommand(skill, opts = {}) {
7716
7802
  const backups = (await listBackups(searchAgents)).filter((b) => b.skill === skill);
7717
7803
  if (backups.length === 0) {
7718
7804
  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"] });
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")] });
7720
7806
  } else {
7721
7807
  log.err(`No backups found for ${skill}.`);
7722
- printNext([{ command: "floom restore --list" }]);
7808
+ printNext([{ command: cliCmd("restore --list") }]);
7723
7809
  }
7724
7810
  process.exitCode = 2;
7725
7811
  return;
@@ -7744,7 +7830,7 @@ async function restoreCommand(skill, opts = {}) {
7744
7830
  }
7745
7831
  }
7746
7832
  if (opts.yes && !opts.dryRun && !agentArg && !isInteractive()) {
7747
- 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.`);
7748
7834
  process.exitCode = 2;
7749
7835
  return;
7750
7836
  }
@@ -7764,7 +7850,7 @@ async function restoreCommand(skill, opts = {}) {
7764
7850
  wouldMutate: true,
7765
7851
  selectedBackup: selected?.timestamp ?? null,
7766
7852
  message: `${skill} would be restored from ${chosen.timestamp}`,
7767
- next: [`floom restore ${skill} --agent ${agent} --backup ${chosen.timestamp} --yes`]
7853
+ next: [cliCmd(`restore ${skill} --agent ${agent} --backup ${chosen.timestamp} --yes`)]
7768
7854
  });
7769
7855
  } else {
7770
7856
  log.heading("Restore plan");
@@ -7794,8 +7880,8 @@ async function restoreCommand(skill, opts = {}) {
7794
7880
  }
7795
7881
  selected = agentBackups[choice - 1];
7796
7882
  } 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.");
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.`);
7799
7885
  process.exitCode = 2;
7800
7886
  return;
7801
7887
  }
@@ -7839,15 +7925,15 @@ async function restoreCommand(skill, opts = {}) {
7839
7925
  preRestoreBackupPath: preRestoreBackup,
7840
7926
  result: "restored",
7841
7927
  message: `Restored ${skill} from ${selected.timestamp}`,
7842
- next: [`floom status --agent ${agent}`]
7928
+ next: [cliCmd(`status --agent ${agent}`)]
7843
7929
  });
7844
7930
  return;
7845
7931
  }
7846
7932
  log.ok(`Restored ${skill} from ${selected.timestamp}`);
7847
- printNext([{ command: `floom status --agent ${agent}` }]);
7933
+ printNext([{ command: cliCmd(`status --agent ${agent}`) }]);
7848
7934
  } catch (error) {
7849
7935
  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"] });
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")] });
7851
7937
  } else {
7852
7938
  log.err(error.message);
7853
7939
  }
@@ -7873,7 +7959,7 @@ async function dashboardCommand() {
7873
7959
  }
7874
7960
  log.blank();
7875
7961
  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" }]);
7962
+ printNext([{ command: cliCmd("login"), description: "sign in to see and sync your Library" }]);
7877
7963
  return;
7878
7964
  }
7879
7965
  const me = await fetchMe().catch(() => null);
@@ -7943,17 +8029,17 @@ async function dashboardCommand() {
7943
8029
  }
7944
8030
  const next = [];
7945
8031
  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" });
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" });
7949
8035
  } else if (anyUpdates) {
7950
- 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" });
7951
8037
  } else if (anyLocalChanges) {
7952
- 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" });
7953
8039
  } else if (!libResult.ok) {
7954
- next.push({ command: "floom status", description: "retry full comparison" });
8040
+ next.push({ command: cliCmd("status"), description: "retry full comparison" });
7955
8041
  }
7956
- next.push({ command: "floom status", description: "see full detail" });
8042
+ next.push({ command: cliCmd("status"), description: "see full detail" });
7957
8043
  printNext(next);
7958
8044
  void [];
7959
8045
  }