@floomhq/floom 2.0.5 → 2.0.7

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
@@ -111,13 +111,13 @@ var require_bcrypt = __commonJS({
111
111
  throw Error("Illegal callback: " + typeof callback);
112
112
  _async(callback);
113
113
  } else
114
- return new Promise(function(resolve3, reject) {
114
+ return new Promise(function(resolve4, reject) {
115
115
  _async(function(err, res) {
116
116
  if (err) {
117
117
  reject(err);
118
118
  return;
119
119
  }
120
- resolve3(res);
120
+ resolve4(res);
121
121
  });
122
122
  });
123
123
  };
@@ -146,13 +146,13 @@ var require_bcrypt = __commonJS({
146
146
  throw Error("Illegal callback: " + typeof callback);
147
147
  _async(callback);
148
148
  } else
149
- return new Promise(function(resolve3, reject) {
149
+ return new Promise(function(resolve4, reject) {
150
150
  _async(function(err, res) {
151
151
  if (err) {
152
152
  reject(err);
153
153
  return;
154
154
  }
155
- resolve3(res);
155
+ resolve4(res);
156
156
  });
157
157
  });
158
158
  };
@@ -197,13 +197,13 @@ var require_bcrypt = __commonJS({
197
197
  throw Error("Illegal callback: " + typeof callback);
198
198
  _async(callback);
199
199
  } else
200
- return new Promise(function(resolve3, reject) {
200
+ return new Promise(function(resolve4, reject) {
201
201
  _async(function(err, res) {
202
202
  if (err) {
203
203
  reject(err);
204
204
  return;
205
205
  }
206
- resolve3(res);
206
+ resolve4(res);
207
207
  });
208
208
  });
209
209
  };
@@ -431,21 +431,21 @@ var require_bcrypt = __commonJS({
431
431
  var utfx2 = {};
432
432
  utfx2.MAX_CODEPOINT = 1114111;
433
433
  utfx2.encodeUTF8 = function(src, dst) {
434
- var cp2 = null;
434
+ var cp3 = null;
435
435
  if (typeof src === "number")
436
- cp2 = src, src = function() {
436
+ cp3 = src, src = function() {
437
437
  return null;
438
438
  };
439
- while (cp2 !== null || (cp2 = src()) !== null) {
440
- if (cp2 < 128)
441
- dst(cp2 & 127);
442
- else if (cp2 < 2048)
443
- dst(cp2 >> 6 & 31 | 192), dst(cp2 & 63 | 128);
444
- else if (cp2 < 65536)
445
- dst(cp2 >> 12 & 15 | 224), dst(cp2 >> 6 & 63 | 128), dst(cp2 & 63 | 128);
439
+ while (cp3 !== null || (cp3 = src()) !== null) {
440
+ if (cp3 < 128)
441
+ dst(cp3 & 127);
442
+ else if (cp3 < 2048)
443
+ dst(cp3 >> 6 & 31 | 192), dst(cp3 & 63 | 128);
444
+ else if (cp3 < 65536)
445
+ dst(cp3 >> 12 & 15 | 224), dst(cp3 >> 6 & 63 | 128), dst(cp3 & 63 | 128);
446
446
  else
447
- dst(cp2 >> 18 & 7 | 240), dst(cp2 >> 12 & 63 | 128), dst(cp2 >> 6 & 63 | 128), dst(cp2 & 63 | 128);
448
- cp2 = null;
447
+ dst(cp3 >> 18 & 7 | 240), dst(cp3 >> 12 & 63 | 128), dst(cp3 >> 6 & 63 | 128), dst(cp3 & 63 | 128);
448
+ cp3 = null;
449
449
  }
450
450
  };
451
451
  utfx2.decodeUTF8 = function(src, dst) {
@@ -487,43 +487,43 @@ var require_bcrypt = __commonJS({
487
487
  if (c2 !== null) dst(c2);
488
488
  };
489
489
  utfx2.UTF8toUTF16 = function(src, dst) {
490
- var cp2 = null;
490
+ var cp3 = null;
491
491
  if (typeof src === "number")
492
- cp2 = src, src = function() {
492
+ cp3 = src, src = function() {
493
493
  return null;
494
494
  };
495
- while (cp2 !== null || (cp2 = src()) !== null) {
496
- if (cp2 <= 65535)
497
- dst(cp2);
495
+ while (cp3 !== null || (cp3 = src()) !== null) {
496
+ if (cp3 <= 65535)
497
+ dst(cp3);
498
498
  else
499
- cp2 -= 65536, dst((cp2 >> 10) + 55296), dst(cp2 % 1024 + 56320);
500
- cp2 = null;
499
+ cp3 -= 65536, dst((cp3 >> 10) + 55296), dst(cp3 % 1024 + 56320);
500
+ cp3 = null;
501
501
  }
502
502
  };
503
503
  utfx2.encodeUTF16toUTF8 = function(src, dst) {
504
- utfx2.UTF16toUTF8(src, function(cp2) {
505
- utfx2.encodeUTF8(cp2, dst);
504
+ utfx2.UTF16toUTF8(src, function(cp3) {
505
+ utfx2.encodeUTF8(cp3, dst);
506
506
  });
507
507
  };
508
508
  utfx2.decodeUTF8toUTF16 = function(src, dst) {
509
- utfx2.decodeUTF8(src, function(cp2) {
510
- utfx2.UTF8toUTF16(cp2, dst);
509
+ utfx2.decodeUTF8(src, function(cp3) {
510
+ utfx2.UTF8toUTF16(cp3, dst);
511
511
  });
512
512
  };
513
- utfx2.calculateCodePoint = function(cp2) {
514
- return cp2 < 128 ? 1 : cp2 < 2048 ? 2 : cp2 < 65536 ? 3 : 4;
513
+ utfx2.calculateCodePoint = function(cp3) {
514
+ return cp3 < 128 ? 1 : cp3 < 2048 ? 2 : cp3 < 65536 ? 3 : 4;
515
515
  };
516
516
  utfx2.calculateUTF8 = function(src) {
517
- var cp2, l = 0;
518
- while ((cp2 = src()) !== null)
519
- l += utfx2.calculateCodePoint(cp2);
517
+ var cp3, l = 0;
518
+ while ((cp3 = src()) !== null)
519
+ l += utfx2.calculateCodePoint(cp3);
520
520
  return l;
521
521
  };
522
522
  utfx2.calculateUTF16asUTF8 = function(src) {
523
523
  var n = 0, l = 0;
524
- utfx2.UTF16toUTF8(src, function(cp2) {
524
+ utfx2.UTF16toUTF8(src, function(cp3) {
525
525
  ++n;
526
- l += utfx2.calculateCodePoint(cp2);
526
+ l += utfx2.calculateCodePoint(cp3);
527
527
  });
528
528
  return [n, l];
529
529
  };
@@ -1916,7 +1916,7 @@ import { promisify } from "node:util";
1916
1916
  var scrypt = promisify(scryptCb);
1917
1917
 
1918
1918
  // ../shared/src/install-targets.ts
1919
- import { homedir } from "node:os";
1919
+ import { homedir, platform } from "node:os";
1920
1920
  import { stat } from "node:fs/promises";
1921
1921
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
1922
1922
  var INSTALL_TARGETS = [
@@ -1956,6 +1956,27 @@ var TARGET_PARENT_DIRS = {
1956
1956
  opencode: ".opencode"
1957
1957
  };
1958
1958
  var SYSTEM_ROOTS = /* @__PURE__ */ new Set(["/", "/etc", "/usr", "/bin", "/sbin", "/var", "/sys", "/proc"]);
1959
+ function xdgConfigHome(homeDir) {
1960
+ const xdg = process.env.XDG_CONFIG_HOME?.trim();
1961
+ if (xdg) return resolve(xdg);
1962
+ if (platform() === "win32") {
1963
+ const appdata = process.env.APPDATA?.trim();
1964
+ if (appdata) return resolve(appdata);
1965
+ }
1966
+ return join(homeDir ?? homedir(), ".config");
1967
+ }
1968
+ var TARGET_SENTINELS = {
1969
+ claude: [],
1970
+ // ~/.claude/ is claude-code-specific; directory alone is fine
1971
+ codex: [],
1972
+ // ~/.codex/ is codex-specific; directory alone is fine
1973
+ cursor: ["mcp.json", "cli-config.json"],
1974
+ // require a Cursor-written file
1975
+ gemini: [],
1976
+ // ~/.gemini/ is gemini-cli-specific; directory alone is fine
1977
+ opencode: []
1978
+ // ~/.config/opencode/ checked below; directory alone is fine
1979
+ };
1959
1980
  function isInsideDir(path, parent) {
1960
1981
  const rel = relative(parent, path);
1961
1982
  return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
@@ -1979,7 +2000,8 @@ function envDirForTarget(target, homeDir) {
1979
2000
  }
1980
2001
  function presetDir(target, opts) {
1981
2002
  const cwd = opts.cwd ?? process.cwd();
1982
- const root = opts.global ? opts.homeDir ?? homedir() : cwd;
2003
+ const homeDir = opts.homeDir ?? homedir();
2004
+ const root = opts.global ? homeDir : cwd;
1983
2005
  switch (target) {
1984
2006
  case "claude":
1985
2007
  return join(root, ".claude", "skills");
@@ -1990,7 +2012,7 @@ function presetDir(target, opts) {
1990
2012
  case "cursor":
1991
2013
  return join(root, ".cursor", "skills");
1992
2014
  case "opencode":
1993
- return join(root, ".opencode", "skills");
2015
+ return opts.global ? join(xdgConfigHome(homeDir), "opencode", "skills") : join(cwd, ".opencode", "skills");
1994
2016
  }
1995
2017
  }
1996
2018
  function resolveInstallDir(args) {
@@ -2020,14 +2042,33 @@ function resolveInstallDir(args) {
2020
2042
  compatibleAgents: COMPATIBLE_AGENTS[target]
2021
2043
  };
2022
2044
  }
2045
+ function agentConfigRoot(target, homeDir) {
2046
+ if (target === "opencode") return join(xdgConfigHome(homeDir), "opencode");
2047
+ return join(homeDir, TARGET_PARENT_DIRS[target]);
2048
+ }
2023
2049
  async function detectInstalledTargets(opts = {}) {
2024
2050
  const homeDir = opts.homeDir ?? homedir();
2025
2051
  const detected = [];
2026
2052
  for (const target of INSTALL_TARGETS) {
2027
- const parentDir = join(homeDir, TARGET_PARENT_DIRS[target]);
2053
+ const configRoot = agentConfigRoot(target, homeDir);
2028
2054
  try {
2029
- const parentStat = await stat(parentDir);
2030
- if (parentStat.isDirectory()) detected.push(target);
2055
+ const dirStat = await stat(configRoot);
2056
+ if (!dirStat.isDirectory()) continue;
2057
+ const sentinels = TARGET_SENTINELS[target];
2058
+ if (sentinels.length === 0) {
2059
+ detected.push(target);
2060
+ continue;
2061
+ }
2062
+ let found = false;
2063
+ for (const sentinel of sentinels) {
2064
+ try {
2065
+ await stat(join(configRoot, sentinel));
2066
+ found = true;
2067
+ break;
2068
+ } catch {
2069
+ }
2070
+ }
2071
+ if (found) detected.push(target);
2031
2072
  } catch {
2032
2073
  }
2033
2074
  }
@@ -2233,6 +2274,10 @@ var InviteCreateResponseSchema = z2.object({
2233
2274
  invite: InviteSchema,
2234
2275
  accept_url: UrlSchema
2235
2276
  }).strict();
2277
+ var InviteLinkRotateResponseSchema = z2.object({
2278
+ invite: InviteSchema,
2279
+ accept_url: UrlSchema
2280
+ }).strict();
2236
2281
  var InvitesListResponseSchema = z2.object({
2237
2282
  invites: z2.array(InviteSchema),
2238
2283
  total: z2.number().int().nonnegative()
@@ -2545,6 +2590,21 @@ var log = {
2545
2590
  },
2546
2591
  blank: () => {
2547
2592
  process.stdout.write("\n");
2593
+ },
2594
+ // A runnable command rendered cyan+bold — the one thing the eye should catch.
2595
+ cmd: (s) => chalk.cyan.bold(s),
2596
+ // Codex-style next-step block: description line, then the command indented in cyan+bold.
2597
+ next: (description, command) => {
2598
+ process.stdout.write(`
2599
+ ${description}
2600
+ ${chalk.cyan.bold(command)}
2601
+ `);
2602
+ },
2603
+ // Aligned-column row for list/status tables.
2604
+ // col widths: [slug col, state col, rest]. Pass strings pre-padded or let row pad them.
2605
+ row: (cols, widths) => {
2606
+ const padded = cols.map((col, i) => widths[i] !== void 0 ? col.padEnd(widths[i]) : col);
2607
+ process.stdout.write(" " + padded.join(" ") + "\n");
2548
2608
  }
2549
2609
  };
2550
2610
 
@@ -2656,7 +2716,7 @@ function normalizeApiUrl(apiUrl) {
2656
2716
  }
2657
2717
 
2658
2718
  // src/lib/machine.ts
2659
- import { hostname, platform, type, release } from "node:os";
2719
+ import { hostname, platform as platform2, type, release } from "node:os";
2660
2720
  import { join as join4 } from "node:path";
2661
2721
  import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
2662
2722
  import { homedir as homedir3 } from "node:os";
@@ -2665,32 +2725,32 @@ import { spawn } from "node:child_process";
2665
2725
  var CONFIG_DIR2 = join4(homedir3(), ".floom");
2666
2726
  var MACHINE_FILE = join4(CONFIG_DIR2, "machine.json");
2667
2727
  async function tryCommand(cmd, args) {
2668
- return new Promise((resolve3) => {
2728
+ return new Promise((resolve4) => {
2669
2729
  let child;
2670
2730
  try {
2671
2731
  child = spawn(cmd, args, { stdio: ["ignore", "pipe", "ignore"] });
2672
2732
  } catch {
2673
- resolve3(null);
2733
+ resolve4(null);
2674
2734
  return;
2675
2735
  }
2676
2736
  let out = "";
2677
2737
  child.stdout?.on("data", (d) => {
2678
2738
  out += d.toString();
2679
2739
  });
2680
- child.on("close", (code) => resolve3(code === 0 ? out.trim() : null));
2681
- child.on("error", () => resolve3(null));
2740
+ child.on("close", (code) => resolve4(code === 0 ? out.trim() : null));
2741
+ child.on("error", () => resolve4(null));
2682
2742
  const timer = setTimeout(() => {
2683
2743
  try {
2684
2744
  child.kill();
2685
2745
  } catch {
2686
2746
  }
2687
- resolve3(null);
2747
+ resolve4(null);
2688
2748
  }, 800);
2689
2749
  child.on("close", () => clearTimeout(timer));
2690
2750
  });
2691
2751
  }
2692
2752
  async function nativeDeviceName() {
2693
- const p = platform();
2753
+ const p = platform2();
2694
2754
  if (p === "darwin") {
2695
2755
  return tryCommand("scutil", ["--get", "ComputerName"]);
2696
2756
  }
@@ -2703,7 +2763,7 @@ async function defaultLabel() {
2703
2763
  const native = await nativeDeviceName();
2704
2764
  const host = (hostname() || "").trim();
2705
2765
  const base = native || host || `${type()}`.slice(0, 40);
2706
- const p = platform();
2766
+ const p = platform2();
2707
2767
  const os = p === "darwin" ? "macOS" : p === "linux" ? "Linux" : p === "win32" ? "Windows" : type();
2708
2768
  const label = base.toLowerCase().includes(os.toLowerCase()) ? base : `${base} \xB7 ${os}`;
2709
2769
  return label.slice(0, 80);
@@ -2738,7 +2798,7 @@ async function getMachineIdentity() {
2738
2798
  }
2739
2799
 
2740
2800
  // src/version.ts
2741
- var VERSION = "2.0.4";
2801
+ var VERSION = "2.0.6";
2742
2802
 
2743
2803
  // src/api-client.ts
2744
2804
  var DEFAULT_TIMEOUT_MS = 2e4;
@@ -2908,8 +2968,33 @@ async function api(path, opts = {}) {
2908
2968
  }
2909
2969
 
2910
2970
  // src/commands/login.ts
2971
+ import { spawn as spawn2 } from "node:child_process";
2972
+ import chalk2 from "chalk";
2973
+ function tryOpenBrowser(url) {
2974
+ if (process.env.FLOOM_NO_OPEN === "1") return;
2975
+ const platform3 = process.platform;
2976
+ let cmd;
2977
+ let args;
2978
+ if (platform3 === "darwin") {
2979
+ cmd = "open";
2980
+ args = [url];
2981
+ } else if (platform3 === "win32") {
2982
+ cmd = "cmd";
2983
+ args = ["/c", "start", "", url];
2984
+ } else {
2985
+ cmd = "xdg-open";
2986
+ args = [url];
2987
+ }
2988
+ try {
2989
+ const child = spawn2(cmd, args, { detached: true, stdio: "ignore" });
2990
+ child.on("error", () => {
2991
+ });
2992
+ child.unref();
2993
+ } catch {
2994
+ }
2995
+ }
2911
2996
  function sleep(ms) {
2912
- return new Promise((resolve3) => setTimeout(resolve3, ms));
2997
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
2913
2998
  }
2914
2999
  async function loginCommand() {
2915
3000
  const session = await api("/cli/device/start", {
@@ -2919,10 +3004,11 @@ async function loginCommand() {
2919
3004
  client: `floom-cli/${VERSION}`
2920
3005
  }
2921
3006
  });
2922
- log.heading("CLI login");
2923
- log.info(`Open: ${session.verification_uri}`);
2924
- log.info(`Code: ${session.user_code}`);
3007
+ log.heading("Sign in to Floom");
3008
+ log.info(`Open: ${session.verification_uri}`);
3009
+ log.info(`Code: ${chalk2.bold(session.user_code)}`);
2925
3010
  log.info("Waiting for browser approval. Press Ctrl+C to cancel.");
3011
+ tryOpenBrowser(session.verification_uri);
2926
3012
  const deadline = new Date(session.expires_at).getTime();
2927
3013
  const interval = Math.max(2, session.poll_interval_seconds) * 1e3;
2928
3014
  while (Date.now() < deadline) {
@@ -2940,7 +3026,9 @@ async function loginCommand() {
2940
3026
  email: token.user?.email ?? "unknown@example.com",
2941
3027
  apiUrl: getApiUrl()
2942
3028
  });
2943
- log.ok(`Logged in to ${token.workspace?.name ?? "Floom"}.`);
3029
+ const emailDisplay = token.user?.email ? ` as ${token.user.email}` : "";
3030
+ log.ok(`Logged in to ${token.workspace?.name ?? "Floom"}${emailDisplay}.`);
3031
+ log.next("Next: pull your workspace skills into your AI agents.", "floom pull");
2944
3032
  return;
2945
3033
  }
2946
3034
  } catch (error) {
@@ -2968,9 +3056,11 @@ import { z as z3 } from "zod";
2968
3056
  // src/commands/sync.ts
2969
3057
  import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
2970
3058
  import { cp, lstat as lstat2, mkdir as mkdir4, readdir as readdir2, readFile as readFile5, rename, rm, stat as stat3, writeFile as writeFile3 } from "node:fs/promises";
2971
- import { dirname, join as join5, sep as sep3 } from "node:path";
3059
+ import { dirname, join as join5, resolve as resolve2, sep as sep3 } from "node:path";
3060
+ import { homedir as osHomedir } from "node:os";
2972
3061
  import { createInterface } from "node:readline/promises";
2973
3062
  import { ZodError } from "zod";
3063
+ import chalk3 from "chalk";
2974
3064
 
2975
3065
  // src/lib/signals.ts
2976
3066
  import { rmSync } from "node:fs";
@@ -3094,6 +3184,146 @@ async function fileExists(path) {
3094
3184
  return false;
3095
3185
  }
3096
3186
  }
3187
+ async function dirExists(path) {
3188
+ try {
3189
+ return (await stat3(path)).isDirectory();
3190
+ } catch {
3191
+ return false;
3192
+ }
3193
+ }
3194
+ async function findGitRoot(startDir, stopAt) {
3195
+ const stop = stopAt ?? osHomedir();
3196
+ let current = resolve2(startDir);
3197
+ while (true) {
3198
+ try {
3199
+ const gitDir = join5(current, ".git");
3200
+ const s = await stat3(gitDir);
3201
+ if (s.isDirectory() || s.isFile()) return current;
3202
+ } catch {
3203
+ }
3204
+ const parent = dirname(current);
3205
+ if (parent === current || current === stop) break;
3206
+ current = parent;
3207
+ }
3208
+ return null;
3209
+ }
3210
+ function targetParentDirName(target) {
3211
+ switch (target) {
3212
+ case "claude":
3213
+ return ".claude";
3214
+ case "codex":
3215
+ return ".codex";
3216
+ case "cursor":
3217
+ return ".cursor";
3218
+ case "gemini":
3219
+ return ".gemini";
3220
+ case "opencode":
3221
+ return ".opencode";
3222
+ }
3223
+ }
3224
+ async function findProjectLocalSkillsDir(target, cwd) {
3225
+ const parentName = targetParentDirName(target);
3226
+ if (!parentName) return null;
3227
+ const home = resolve2(osHomedir());
3228
+ const gitRoot = await findGitRoot(cwd);
3229
+ const stopAt = gitRoot ?? home;
3230
+ let current = resolve2(cwd);
3231
+ while (true) {
3232
+ if (current !== home) {
3233
+ const agentDir = join5(current, parentName);
3234
+ if (await dirExists(agentDir)) {
3235
+ return join5(agentDir, "skills");
3236
+ }
3237
+ }
3238
+ const parent = dirname(current);
3239
+ if (parent === current || current === stopAt) break;
3240
+ current = parent;
3241
+ }
3242
+ if (stopAt !== home) {
3243
+ const agentDir = join5(stopAt, parentName);
3244
+ if (await dirExists(agentDir)) {
3245
+ return join5(agentDir, "skills");
3246
+ }
3247
+ }
3248
+ return null;
3249
+ }
3250
+ async function resolvePullDirs(target, cwd = process.cwd(), homeDir, globalOnly = false) {
3251
+ const primary = resolveInstallDir({ target, global: true, homeDir }).dir;
3252
+ if (globalOnly) {
3253
+ return uniqueResolvedDirs([primary]);
3254
+ }
3255
+ const projectLocalSkillsDir = await findProjectLocalSkillsDir(target, cwd);
3256
+ if (!projectLocalSkillsDir) {
3257
+ return uniqueResolvedDirs([primary]);
3258
+ }
3259
+ return uniqueResolvedDirs([primary, projectLocalSkillsDir]);
3260
+ }
3261
+ async function hasStatusVisibleProjectLocalDir(dir) {
3262
+ if (await fileExists(manifestPath(dir))) return true;
3263
+ return await dirExists(dir) && await hasAnySkillSubdir(dir);
3264
+ }
3265
+ async function resolveProjectLocalStatusDirs(target, cwd = process.cwd()) {
3266
+ const projectLocal = presetDir(target, { cwd });
3267
+ if (!await hasStatusVisibleProjectLocalDir(projectLocal)) return [];
3268
+ return uniqueResolvedDirs([projectLocal]);
3269
+ }
3270
+ async function resolveStatusDirs(target, cwd = process.cwd(), homeDir) {
3271
+ const primary = resolveInstallDir({ target, global: true, homeDir }).dir;
3272
+ const projectLocalDirs = await resolveProjectLocalStatusDirs(target, cwd);
3273
+ return uniqueResolvedDirs([primary, ...projectLocalDirs]);
3274
+ }
3275
+ async function resolveAutoStatusTargets(cwd = process.cwd(), homeDir) {
3276
+ const detected = new Set(await detectInstalledTargets({ homeDir }));
3277
+ const resolved = [];
3278
+ for (const target of INSTALL_TARGETS) {
3279
+ if (detected.has(target)) {
3280
+ resolved.push({ target, dirs: await resolveStatusDirs(target, cwd, homeDir) });
3281
+ continue;
3282
+ }
3283
+ const projectLocalDirs = await resolveProjectLocalStatusDirs(target, cwd);
3284
+ if (projectLocalDirs.length > 0) resolved.push({ target, dirs: projectLocalDirs });
3285
+ }
3286
+ return resolved;
3287
+ }
3288
+ async function hasAnySkillSubdir(root) {
3289
+ let entries;
3290
+ try {
3291
+ entries = await readdir2(root, { withFileTypes: true });
3292
+ } catch {
3293
+ return false;
3294
+ }
3295
+ for (const entry of entries) {
3296
+ if (!entry.isDirectory()) continue;
3297
+ if (await fileExists(join5(root, entry.name, "SKILL.md"))) return true;
3298
+ }
3299
+ return false;
3300
+ }
3301
+ async function listSkillSubdirs(root) {
3302
+ let entries;
3303
+ try {
3304
+ entries = await readdir2(root, { withFileTypes: true });
3305
+ } catch {
3306
+ return [];
3307
+ }
3308
+ const matches = [];
3309
+ for (const entry of entries) {
3310
+ if (!entry.isDirectory()) continue;
3311
+ const candidate = join5(root, entry.name);
3312
+ if (await fileExists(join5(candidate, "SKILL.md"))) matches.push(candidate);
3313
+ }
3314
+ return matches.sort();
3315
+ }
3316
+ function uniqueResolvedDirs(dirs) {
3317
+ const seen = /* @__PURE__ */ new Set();
3318
+ const unique = [];
3319
+ for (const dir of dirs) {
3320
+ const resolved = resolve2(dir);
3321
+ if (seen.has(resolved)) continue;
3322
+ seen.add(resolved);
3323
+ unique.push(resolved);
3324
+ }
3325
+ return unique;
3326
+ }
3097
3327
  async function liveSkillHash(root, workspaceSlug, skill) {
3098
3328
  const slug = safeSkillSlug(skill.slug);
3099
3329
  const fileHashes = [];
@@ -3251,41 +3481,119 @@ function printNoDetectedTargets() {
3251
3481
  log.info(` Or pick one explicitly: npx -y @floomhq/floom pull --target claude`);
3252
3482
  log.info(` More help: https://floom.dev/docs#agents`);
3253
3483
  }
3254
- async function statusCommand(options) {
3484
+ async function runStatusForDir(target, dir, deps) {
3485
+ const hasFloomManifest = await fileExists(manifestPath(dir));
3486
+ if (!hasFloomManifest) {
3487
+ const localSkillDirs = await listSkillSubdirs(dir);
3488
+ if (localSkillDirs.length > 0) {
3489
+ printRawLocalSkillsHint(target, dir, localSkillDirs);
3490
+ return { ok: true };
3491
+ }
3492
+ }
3493
+ try {
3494
+ const status = await statusLibrary(target, { ...deps, installDir: dir });
3495
+ printFloomManagedStatus(target, dir, status);
3496
+ return { ok: true };
3497
+ } catch (error) {
3498
+ return { ok: false, error };
3499
+ }
3500
+ }
3501
+ function shellSingleQuote(value) {
3502
+ return `'${value.replace(/'/g, "'\\''")}'`;
3503
+ }
3504
+ function printRawLocalSkillsHint(target, dir, localSkillDirs) {
3505
+ log.heading(`Local skills found (${target})`);
3506
+ log.info(`dir ${dir}`);
3507
+ log.info(` Floom does not manage these yet. Push them to your library:`);
3508
+ for (const skillDir of localSkillDirs) {
3509
+ log.info(` - ${skillDir}`);
3510
+ }
3511
+ log.info(` Push a single skill: npx -y @floomhq/floom push ${shellSingleQuote(localSkillDirs[0])}`);
3512
+ log.info(` Push them all: npx -y @floomhq/floom push ${shellSingleQuote(dir)}`);
3513
+ }
3514
+ function stateLabel(state) {
3515
+ switch (state) {
3516
+ case "active":
3517
+ return chalk3.green("up to date");
3518
+ case "stale":
3519
+ return chalk3.yellow("needs pull");
3520
+ case "dirty":
3521
+ return chalk3.yellow("local edits");
3522
+ case "conflict":
3523
+ return chalk3.red("conflict");
3524
+ case "missing":
3525
+ return "not installed";
3526
+ case "unsupported_target":
3527
+ return "other agent";
3528
+ }
3529
+ }
3530
+ function printFloomManagedStatus(target, dir, status) {
3531
+ log.info(`${status.workspaceName} \xB7 ${target} \xB7 ${dir}`);
3532
+ log.blank();
3533
+ const slugWidth = Math.max(...status.skills.map((s) => s.slug.length), 4);
3534
+ const labelWidth = 14;
3535
+ for (const line of status.skills) {
3536
+ const label = stateLabel(line.state);
3537
+ process.stdout.write(` ${line.slug.padEnd(slugWidth)} ${label.padEnd(labelWidth + (label.length - stripAnsiLength(label)))} ${line.version}
3538
+ `);
3539
+ }
3540
+ log.blank();
3541
+ const needsPull = status.skills.filter((s) => s.state === "stale").length;
3542
+ const localEdits = status.skills.filter((s) => s.state === "dirty").length;
3543
+ const conflicts = status.skills.filter((s) => s.state === "conflict").length;
3544
+ const parts = [`${status.skills.length} skill${status.skills.length !== 1 ? "s" : ""}`];
3545
+ if (needsPull > 0) parts.push(`${needsPull} needs pull`);
3546
+ if (localEdits > 0) parts.push(`${localEdits} with local edits`);
3547
+ if (conflicts > 0) parts.push(`${conflicts} conflict${conflicts !== 1 ? "s" : ""}`);
3548
+ log.info(parts.join(" \xB7 "));
3549
+ const showPull = needsPull > 0 || conflicts > 0;
3550
+ const showPush = localEdits > 0 || conflicts > 0;
3551
+ if (showPull || showPush) {
3552
+ log.blank();
3553
+ log.info("Next:");
3554
+ if (showPull) log.info(` ${log.cmd("floom pull")} update skills that are behind`);
3555
+ if (showPush) log.info(` ${log.cmd("floom push <dir>")} publish your local edits`);
3556
+ }
3557
+ }
3558
+ var ANSI_STRIP_RE = /\x1b\[[0-9;]*m/g;
3559
+ function stripAnsiLength(s) {
3560
+ return s.replace(ANSI_STRIP_RE, "").length;
3561
+ }
3562
+ async function statusCommand(options, deps = {}) {
3255
3563
  if (!options.target) {
3256
- const targets = await detectInstalledTargets();
3257
- if (targets.length === 0) {
3564
+ const targetDirs = await resolveAutoStatusTargets();
3565
+ if (targetDirs.length === 0) {
3258
3566
  printNoDetectedTargets();
3259
3567
  process.exitCode = 1;
3260
3568
  return;
3261
3569
  }
3262
- printDetectedTargets(targets);
3263
- const results = [];
3264
- for (const target2 of targets) {
3265
- try {
3266
- results.push({ target: target2, ok: true, status: await statusLibrary(target2) });
3267
- } catch (error) {
3268
- results.push({ target: target2, ok: false, error });
3269
- }
3270
- }
3271
- for (const result of results) {
3272
- if (!result.ok) {
3273
- log.err(`${result.target} ${result.error.message}`);
3274
- continue;
3570
+ printDetectedTargets(targetDirs.map(({ target: target2 }) => target2));
3571
+ let anyFailed2 = false;
3572
+ for (const { target: target2, dirs } of targetDirs) {
3573
+ for (const dir of dirs) {
3574
+ const result = await runStatusForDir(target2, dir, deps);
3575
+ if (!result.ok) {
3576
+ log.err(`${target2} ${dir} ${result.error.message}`);
3577
+ anyFailed2 = true;
3578
+ }
3275
3579
  }
3276
- log.heading(`${result.status.workspaceName} (${result.target})`);
3277
- for (const line of result.status.skills) log.info(`${line.slug} ${line.state} ${line.version}`);
3278
3580
  }
3279
- if (results.some((result) => !result.ok)) process.exitCode = 1;
3581
+ if (anyFailed2) process.exitCode = 1;
3280
3582
  return;
3281
3583
  }
3282
3584
  const target = assertInstallTarget(options.target);
3283
- const status = await statusLibrary(target);
3284
- log.heading(`${status.workspaceName} (${target})`);
3285
- for (const line of status.skills) log.info(`${line.slug} ${line.state} ${line.version}`);
3585
+ let anyFailed = false;
3586
+ for (const dir of await resolveStatusDirs(target)) {
3587
+ const result = await runStatusForDir(target, dir, deps);
3588
+ if (!result.ok) {
3589
+ log.err(`${target} ${dir} ${result.error.message}`);
3590
+ anyFailed = true;
3591
+ }
3592
+ }
3593
+ if (anyFailed) process.exitCode = 1;
3286
3594
  }
3287
3595
  async function statusLibrary(target, deps = {}) {
3288
- const resolved = resolveInstallDir({ target, global: true });
3596
+ const resolved = { dir: deps.installDir ?? resolveInstallDir({ target, global: true }).dir };
3289
3597
  const remote = await (deps.fetchLibrary ?? fetchLibrary)(target, "status");
3290
3598
  const manifest = await readManifest(resolved.dir);
3291
3599
  if (!manifest) {
@@ -3323,7 +3631,7 @@ async function statusLibrary(target, deps = {}) {
3323
3631
  }
3324
3632
  return { workspaceName: remote.workspace.name, skills };
3325
3633
  }
3326
- async function pullCommand(options) {
3634
+ async function pullCommand(options, deps = {}) {
3327
3635
  const cleanup = installCancellationHandler();
3328
3636
  try {
3329
3637
  if (!options.target) {
@@ -3338,39 +3646,55 @@ async function pullCommand(options) {
3338
3646
  log.info("Cancelled.");
3339
3647
  return;
3340
3648
  }
3649
+ const dirsByTarget = /* @__PURE__ */ new Map();
3341
3650
  for (const target2 of targets) {
3342
- const resolved2 = resolveInstallDir({ target: target2, global: true });
3343
- cleanup.trackDir(join5(resolved2.dir, ".floom", "tmp"));
3651
+ const dirs2 = await resolvePullDirs(target2, process.cwd(), void 0, options.globalOnly);
3652
+ dirsByTarget.set(target2, dirs2);
3653
+ for (const dir of dirs2) cleanup.trackDir(join5(dir, ".floom", "tmp"));
3344
3654
  }
3345
3655
  const results = [];
3346
3656
  for (const target2 of targets) {
3347
3657
  try {
3348
- const result2 = await pullLibrary(target2);
3349
- results.push({ target: target2, ok: true, ...result2 });
3658
+ const dirs2 = dirsByTarget.get(target2) ?? [];
3659
+ let skillCount2 = 0;
3660
+ for (const dir of dirs2) {
3661
+ const result = await pullLibrary(target2, { ...deps, installDir: dir });
3662
+ skillCount2 = result.skillCount;
3663
+ }
3664
+ results.push({ target: target2, ok: true, skillCount: skillCount2, dirs: dirs2 });
3350
3665
  } catch (error) {
3351
3666
  results.push({ target: target2, ok: false, error });
3352
3667
  }
3353
3668
  }
3354
3669
  log.heading("Pull summary:");
3355
- for (const result2 of results) {
3356
- if (result2.ok) log.ok(`${result2.target} ${result2.skillCount} skills ${result2.dir}`);
3357
- else log.err(`${result2.target} ${result2.error.message}`);
3670
+ for (const result of results) {
3671
+ if (result.ok) {
3672
+ log.ok(`${result.target} ${result.skillCount} skills`);
3673
+ for (const dir of result.dirs) log.info(` \u2192 ${dir}`);
3674
+ } else {
3675
+ log.err(`${result.target} ${result.error.message}`);
3676
+ }
3358
3677
  }
3359
- if (results.some((result2) => !result2.ok)) process.exitCode = 1;
3678
+ if (results.some((result) => !result.ok)) process.exitCode = 1;
3360
3679
  return;
3361
3680
  }
3362
3681
  const target = assertInstallTarget(options.target);
3363
- const resolved = resolveInstallDir({ target, global: true });
3364
- cleanup.trackDir(join5(resolved.dir, ".floom", "tmp"));
3365
- const result = await pullLibrary(target);
3366
- log.ok(`Pulled ${result.skillCount} skills into ${target} (${result.dir}).`);
3682
+ const dirs = await resolvePullDirs(target, process.cwd(), void 0, options.globalOnly);
3683
+ for (const dir of dirs) cleanup.trackDir(join5(dir, ".floom", "tmp"));
3684
+ let skillCount = 0;
3685
+ for (const dir of dirs) {
3686
+ const result = await pullLibrary(target, { ...deps, installDir: dir });
3687
+ skillCount = result.skillCount;
3688
+ }
3689
+ log.ok(`Pulled ${skillCount} skills into ${target}.`);
3690
+ for (const dir of dirs) log.info(` \u2192 ${dir}`);
3367
3691
  log.info(`This syncs ${target} only. For another agent: npx -y @floomhq/floom pull --target <claude|codex|cursor|gemini|opencode>`);
3368
3692
  } finally {
3369
3693
  cleanup.dispose();
3370
3694
  }
3371
3695
  }
3372
3696
  async function pullLibrary(target, deps = {}) {
3373
- const resolved = resolveInstallDir({ target, global: true });
3697
+ const resolved = { dir: deps.installDir ?? resolveInstallDir({ target, global: true }).dir };
3374
3698
  const remote = await (deps.fetchLibrary ?? fetchLibrary)(target, "pull");
3375
3699
  for (const skill of remote.skills) safeSkillSlug(skill.slug);
3376
3700
  const manifest = await readManifest(resolved.dir);
@@ -3630,13 +3954,19 @@ function createMcpServer(deps = {}) {
3630
3954
  return server;
3631
3955
  }
3632
3956
  async function mcpCommand() {
3957
+ if (process.stderr.isTTY || process.stdin.isTTY) {
3958
+ process.stderr.write("Floom MCP server running (stdio).\n");
3959
+ process.stderr.write("This is launched by your AI agent, not run by hand.\n");
3960
+ process.stderr.write("Add it to Claude: claude mcp add floom -- npx -y @floomhq/floom mcp\n");
3961
+ process.stderr.write("Ctrl+C to stop.\n");
3962
+ }
3633
3963
  const server = createMcpServer();
3634
3964
  const transport = new StdioServerTransport();
3635
3965
  await server.connect(transport);
3636
3966
  }
3637
3967
 
3638
3968
  // src/commands/push.ts
3639
- import { basename, join as join6, resolve as resolve2 } from "node:path";
3969
+ import { basename, join as join6, resolve as resolve3 } from "node:path";
3640
3970
  import { readdir as readdir3, readFile as readFile6, stat as stat4 } from "node:fs/promises";
3641
3971
  function parseConcurrency(value) {
3642
3972
  const raw = value ?? 6;
@@ -3701,7 +4031,7 @@ async function runBounded(items, concurrency, worker) {
3701
4031
  async function pushCommand(dir = ".", options = {}, deps = {}) {
3702
4032
  const cleanup = installCancellationHandler();
3703
4033
  try {
3704
- const root = resolve2(dir);
4034
+ const root = resolve3(dir);
3705
4035
  const dirStat = await stat4(root).catch(() => null);
3706
4036
  if (!dirStat || !dirStat.isDirectory()) {
3707
4037
  throw new Error(
@@ -3713,6 +4043,7 @@ async function pushCommand(dir = ".", options = {}, deps = {}) {
3713
4043
  if (await hasSkillMarkdown(root)) {
3714
4044
  const result = await pushOneSkill(root, pushApi);
3715
4045
  log.ok(`Pushed ${result.skill.slug} ${result.skill.latest.display}.`);
4046
+ log.next("Next: pull this skill into your AI agents.", "floom pull");
3716
4047
  return;
3717
4048
  }
3718
4049
  const skillDirs = await findImmediateSkillDirs(root);
@@ -3729,7 +4060,7 @@ async function pushCommand(dir = ".", options = {}, deps = {}) {
3729
4060
  try {
3730
4061
  const result = await pushOneSkill(skillDir, pushApi);
3731
4062
  pushed += 1;
3732
- log.info(`\u2713 ${result.skill.slug} ${result.skill.latest.display}`);
4063
+ log.ok(`${result.skill.slug} ${result.skill.latest.display}`);
3733
4064
  } catch (error) {
3734
4065
  errors.push({ slug, message: error.message });
3735
4066
  }
@@ -3766,19 +4097,196 @@ async function deleteCommand(slug, opts = {}, deps = defaultDeps) {
3766
4097
  async function listCommand() {
3767
4098
  const result = await api("/skills", { authRequired: true });
3768
4099
  if (result.total === 0) {
3769
- log.info("No skills in this workspace.");
4100
+ log.info("No skills in this workspace yet.");
4101
+ log.next("Next: publish your first skill.", "floom push ./path/to/skill-folder");
3770
4102
  return;
3771
4103
  }
4104
+ const slugWidth = Math.max(...result.skills.map((s) => s.slug.length), 4);
4105
+ const verWidth = Math.max(...result.skills.map((s) => s.latest_version.display.length), 3);
3772
4106
  for (const skill of result.skills) {
3773
- log.info(`${skill.slug} ${skill.latest_version.display} ${skill.title}`);
4107
+ log.row([skill.slug, skill.latest_version.display, skill.title], [slugWidth, verWidth, 0]);
4108
+ }
4109
+ }
4110
+
4111
+ // src/commands/sync-command.ts
4112
+ import { cp as cp2, mkdtemp, readdir as readdir4, rm as rm2, stat as stat5 } from "node:fs/promises";
4113
+ import { tmpdir } from "node:os";
4114
+ import { join as join7 } from "node:path";
4115
+ import { createInterface as createInterface2 } from "node:readline/promises";
4116
+ async function hasSkillMarkdown2(dir) {
4117
+ try {
4118
+ const skillMd = await stat5(join7(dir, "SKILL.md"));
4119
+ return skillMd.isFile();
4120
+ } catch {
4121
+ return false;
4122
+ }
4123
+ }
4124
+ async function findImmediateSkillDirs2(root) {
4125
+ let entries;
4126
+ try {
4127
+ entries = await readdir4(root, { withFileTypes: true });
4128
+ } catch (error) {
4129
+ const err = error;
4130
+ if (err.code === "ENOENT") return [];
4131
+ throw error;
4132
+ }
4133
+ const dirs = entries.filter((entry) => entry.isDirectory() && entry.name !== ".floom").map((entry) => ({ slug: entry.name, dir: join7(root, entry.name) })).sort((a, b) => a.slug.localeCompare(b.slug));
4134
+ const checks = await Promise.all(dirs.map(async (entry) => await hasSkillMarkdown2(entry.dir) ? entry : null));
4135
+ return checks.filter((entry) => Boolean(entry));
4136
+ }
4137
+ function formatSlugs(slugs) {
4138
+ return slugs.length > 0 ? slugs.join(", ") : "none";
4139
+ }
4140
+ function printPreview(plan) {
4141
+ log.info(
4142
+ `Pull: ${plan.pull.length} skills (${formatSlugs(plan.pull.map((skill) => skill.slug))}). Push: ${plan.push.length} skills (${formatSlugs(plan.push.map((skill) => skill.slug))}). Skip (conflict): ${plan.conflicts.length} skills (${formatSlugs(plan.conflicts.map((skill) => skill.slug))}).`
4143
+ );
4144
+ if (plan.conflicts.length > 0) {
4145
+ const count = plan.conflicts.length;
4146
+ const noun = count === 1 ? "skill changed" : "skills changed";
4147
+ log.err(`${count} ${noun} both locally and on the server: Floom won't guess which wins.`);
4148
+ for (const skill of plan.conflicts) {
4149
+ log.err(` - ${skill.slug} (${skill.version})`);
4150
+ log.err(` Keep the server version: floom pull --target ${plan.target}`);
4151
+ log.err(` Keep your local version: floom push <${skill.slug}-dir>`);
4152
+ }
4153
+ log.err("Your local copy is always backed up to .floom/backups/ first.");
4154
+ log.err("More: https://floom.dev/docs#conflicts");
4155
+ }
4156
+ }
4157
+ async function defaultConfirmProceed() {
4158
+ if (!process.stdin.isTTY) {
4159
+ log.err("Aborted: confirmation is required in non-interactive mode. Re-run with --yes to proceed.");
4160
+ return false;
4161
+ }
4162
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
4163
+ try {
4164
+ const answer = (await rl.question("Proceed? [Y/n] ")).trim().toLowerCase();
4165
+ return answer === "" || answer === "y" || answer === "yes";
4166
+ } finally {
4167
+ rl.close();
4168
+ }
4169
+ }
4170
+ async function resolveTarget(options, deps) {
4171
+ if (options.target) return assertInstallTarget(options.target);
4172
+ const targets = await (deps.detectInstalledTargets ?? detectInstalledTargets)();
4173
+ if (targets.length === 0) {
4174
+ log.err("No AI agent recognized on this machine. Re-run with --target <claude|codex|cursor|gemini|opencode>.");
4175
+ process.exitCode = 1;
4176
+ return null;
4177
+ }
4178
+ if (targets.length > 1) {
4179
+ log.err(`Multiple AI agents detected (${targets.join(", ")}). Re-run with --target <agent>.`);
4180
+ process.exitCode = 1;
4181
+ return null;
4182
+ }
4183
+ return targets[0];
4184
+ }
4185
+ async function createPushSnapshots(pushes) {
4186
+ const root = await mkdtemp(join7(tmpdir(), "floom-sync-push-"));
4187
+ const dirs = [];
4188
+ try {
4189
+ for (const push of pushes) {
4190
+ const dest = join7(root, push.slug);
4191
+ await cp2(push.dir, dest, { recursive: true });
4192
+ dirs.push({ slug: push.slug, dir: dest });
4193
+ }
4194
+ return { root, dirs };
4195
+ } catch (error) {
4196
+ await rm2(root, { recursive: true, force: true });
4197
+ throw error;
4198
+ }
4199
+ }
4200
+ async function buildPlan(options, deps) {
4201
+ const target = await resolveTarget(options, deps);
4202
+ if (!target) return null;
4203
+ const status = await (deps.statusLibrary ?? statusLibrary)(target);
4204
+ const dirtySlugs = new Set(status.skills.filter((skill) => skill.state === "dirty").map((skill) => skill.slug));
4205
+ const installDir = resolveInstallDir({ target, global: true }).dir;
4206
+ const localSkillDirs = await findImmediateSkillDirs2(installDir);
4207
+ return {
4208
+ target,
4209
+ pull: status.skills.filter((skill) => skill.state === "missing" || skill.state === "stale"),
4210
+ push: localSkillDirs.filter((entry) => dirtySlugs.has(entry.slug)),
4211
+ conflicts: status.skills.filter((skill) => skill.state === "conflict")
4212
+ };
4213
+ }
4214
+ async function syncCommand(options = {}, deps = {}) {
4215
+ const plan = await buildPlan(options, deps);
4216
+ if (!plan) return;
4217
+ if (plan.pull.length === 0 && plan.push.length === 0 && plan.conflicts.length === 0) {
4218
+ log.info("Already in sync.");
4219
+ return;
4220
+ }
4221
+ printPreview(plan);
4222
+ if (plan.conflicts.length > 0) {
4223
+ process.exitCode = 1;
4224
+ return;
4225
+ }
4226
+ if (plan.pull.length === 0 && plan.push.length === 0) return;
4227
+ if (options.yes) {
4228
+ log.info("Proceeding (--yes).");
4229
+ } else if (!await (deps.confirmProceed ?? defaultConfirmProceed)()) {
4230
+ log.info("Aborted.");
4231
+ return;
4232
+ }
4233
+ const snapshots = await createPushSnapshots(plan.push);
4234
+ const pushFailures = [];
4235
+ let pushed = 0;
4236
+ let preserveSnapshotsForRecovery = false;
4237
+ try {
4238
+ try {
4239
+ await (deps.pullLibrary ?? pullLibrary)(plan.target);
4240
+ } catch (error) {
4241
+ log.err(`Pull failed: ${error.message}`);
4242
+ process.exitCode = 1;
4243
+ return;
4244
+ }
4245
+ for (const snapshot of snapshots.dirs) {
4246
+ try {
4247
+ await (deps.pushSkill ?? ((dir) => pushCommand(dir)))(snapshot.dir);
4248
+ pushed += 1;
4249
+ } catch (error) {
4250
+ pushFailures.push({
4251
+ slug: snapshot.slug,
4252
+ message: error.message,
4253
+ snapshotDir: snapshot.dir
4254
+ });
4255
+ }
4256
+ }
4257
+ } finally {
4258
+ if (pushFailures.length === 0) {
4259
+ await rm2(snapshots.root, { recursive: true, force: true });
4260
+ } else {
4261
+ preserveSnapshotsForRecovery = true;
4262
+ }
4263
+ }
4264
+ if (pushFailures.length > 0) {
4265
+ log.err(`Push failed for ${pushFailures.length} of ${plan.push.length} skill(s).`);
4266
+ log.err("Your local edits are preserved in two places (nothing is lost):");
4267
+ log.err(` 1. Snapshot taken before pull: ${snapshots.root}`);
4268
+ log.err(` 2. Pull backup of pre-pull dir: ${plan.target} \u2192 .floom/backups/<latest>/`);
4269
+ log.err("");
4270
+ log.err("Failed skills:");
4271
+ for (const failure of pushFailures) {
4272
+ log.err(` - ${failure.slug}: ${failure.message}`);
4273
+ log.err(` snapshot at: ${failure.snapshotDir}`);
4274
+ }
4275
+ log.err("");
4276
+ log.err(`Re-run \`floom sync --target ${plan.target}\` after the network recovers.`);
4277
+ process.exitCode = 1;
4278
+ }
4279
+ void preserveSnapshotsForRecovery;
4280
+ if (pushFailures.length === 0) {
4281
+ log.ok(`Sync complete. Pulled ${plan.pull.length} skills. Pushed ${pushed}/${plan.push.length} skills.`);
3774
4282
  }
3775
4283
  }
3776
4284
 
3777
4285
  // src/commands/rename-machine.ts
3778
4286
  import { readFile as readFile7, writeFile as writeFile4 } from "node:fs/promises";
3779
- import { join as join7 } from "node:path";
4287
+ import { join as join8 } from "node:path";
3780
4288
  import { homedir as homedir4 } from "node:os";
3781
- var MACHINE_FILE2 = join7(homedir4(), ".floom", "machine.json");
4289
+ var MACHINE_FILE2 = join8(homedir4(), ".floom", "machine.json");
3782
4290
  async function renameMachineCommand(newLabel, _opts) {
3783
4291
  const trimmed = newLabel.trim().slice(0, 80);
3784
4292
  if (!trimmed) {
@@ -3817,8 +4325,8 @@ async function renameMachineCommand(newLabel, _opts) {
3817
4325
  }
3818
4326
 
3819
4327
  // src/commands/add.ts
3820
- import { mkdir as mkdir5, rm as rm2, writeFile as writeFile5 } from "node:fs/promises";
3821
- import { join as join8 } from "node:path";
4328
+ import { mkdir as mkdir5, rm as rm3, writeFile as writeFile5 } from "node:fs/promises";
4329
+ import { join as join9 } from "node:path";
3822
4330
  var TOKEN_RE = /^fls_[A-Za-z0-9_-]{32,}$/;
3823
4331
  function parseToken(input) {
3824
4332
  const trimmed = input.trim();
@@ -3971,27 +4479,27 @@ async function addCommand(input, opts = {}) {
3971
4479
  safeRemotePath2(file.path);
3972
4480
  }
3973
4481
  const cleanup = installCancellationHandler();
3974
- const tempDir = join8(resolved.dir, ".floom", "tmp", `${slug}-add-${Date.now()}`);
4482
+ const tempDir = join9(resolved.dir, ".floom", "tmp", `${slug}-add-${Date.now()}`);
3975
4483
  cleanup.trackDir(tempDir);
3976
4484
  try {
3977
4485
  await mkdir5(tempDir, { recursive: true });
3978
4486
  for (const file of shareData.file_contents) {
3979
4487
  const safePath = safeRemotePath2(file.path);
3980
- const dest = join8(tempDir, ...safePath.split("/"));
4488
+ const dest = join9(tempDir, ...safePath.split("/"));
3981
4489
  const destDir = dest.substring(0, dest.lastIndexOf("/"));
3982
4490
  if (destDir !== tempDir) await mkdir5(destDir, { recursive: true });
3983
4491
  await writeFile5(dest, bytesForShareFile(file));
3984
4492
  }
3985
- const finalDir = join8(resolved.dir, slug);
3986
- const replacedDir = join8(resolved.dir, ".floom", "tmp", `${slug}-previous-${Date.now()}`);
4493
+ const finalDir = join9(resolved.dir, slug);
4494
+ const replacedDir = join9(resolved.dir, ".floom", "tmp", `${slug}-previous-${Date.now()}`);
3987
4495
  let movedExisting = false;
3988
4496
  try {
3989
4497
  const { rename: rename2, rm: rmFs } = await import("node:fs/promises");
3990
4498
  await mkdir5(resolved.dir, { recursive: true });
3991
- const { stat: stat5 } = await import("node:fs/promises");
4499
+ const { stat: stat6 } = await import("node:fs/promises");
3992
4500
  let existingDir = false;
3993
4501
  try {
3994
- await stat5(finalDir);
4502
+ await stat6(finalDir);
3995
4503
  existingDir = true;
3996
4504
  } catch {
3997
4505
  }
@@ -4025,7 +4533,7 @@ async function addCommand(input, opts = {}) {
4025
4533
  } finally {
4026
4534
  cleanup.dispose();
4027
4535
  try {
4028
- await rm2(tempDir, { recursive: true, force: true });
4536
+ await rm3(tempDir, { recursive: true, force: true });
4029
4537
  } catch {
4030
4538
  }
4031
4539
  }
@@ -4075,18 +4583,30 @@ async function whoamiCommand() {
4075
4583
  }
4076
4584
  }
4077
4585
  var program = new Command();
4078
- program.name("floom").description("Floom CLI \u2014 one shared skill library, pulled into the AI agent you choose.").version(VERSION);
4079
- program.command("login").description("Log in via browser device flow.").action(loginCommand);
4080
- program.command("logout").description("Clear local Floom auth.").action(logoutCommand);
4081
- program.command("whoami").description("Show local auth state.").action(whoamiCommand);
4082
- program.command("push [dir]").description("Push a skill folder.").option("--concurrency <n>", "Bulk push concurrency, 1-16", "6").action(pushCommand);
4083
- program.command("delete <slug>").description("Delete a workspace skill.").option("--yes", "Skip confirmation").action((slug, opts) => deleteCommand(slug, opts));
4084
- program.command("pull").description("Pull the whole workspace library.").option("--target <target>", "claude | codex | cursor | gemini | opencode").action(pullCommand);
4586
+ program.name("floom").description("Floom: one shared skill library, synced across every AI agent.").version(VERSION).addHelpCommand(false).hook("preAction", () => {
4587
+ });
4588
+ program.action(() => {
4589
+ log.info("Floom: one shared skill library, synced across every AI agent.");
4590
+ log.blank();
4591
+ log.info("New here?");
4592
+ log.info(` 1. ${log.cmd("floom login")} sign in`);
4593
+ log.info(` 2. ${log.cmd("floom pull")} get your team's skills (or ${log.cmd("floom push <dir>")} to publish)`);
4594
+ log.info(` 3. ${log.cmd("floom status")} see what's synced`);
4595
+ log.blank();
4596
+ log.info(`All commands: ${log.cmd("floom --help")}`);
4597
+ });
4598
+ program.command("login").description("Sign in via browser.").action(loginCommand);
4599
+ program.command("logout").description("Sign out and clear local auth.").action(logoutCommand);
4600
+ program.command("whoami").description("Show who you are signed in as.").action(whoamiCommand);
4601
+ program.command("push [dir]").description("Publish a skill folder to your workspace.").option("--concurrency <n>", "Bulk push concurrency, 1-16", "6").action(pushCommand);
4602
+ program.command("delete <slug>").description("Delete a skill from your workspace.").option("--yes", "Skip confirmation").action((slug, opts) => deleteCommand(slug, opts));
4603
+ program.command("pull").description("Pull the workspace library into your AI agents.").option("--target <target>", "claude | codex | cursor | gemini | opencode").option("--global-only", "Write only to the global agent dir; skip project-local .claude/skills/ etc.").action((opts) => pullCommand({ target: opts.target, globalOnly: opts.globalOnly }));
4604
+ program.command("sync").description("Pull remote changes, then push any local edits.").option("--target <target>", "claude | codex | cursor | gemini | opencode").option("--yes", "Skip confirmation").action(syncCommand);
4085
4605
  program.command("list").description("List workspace skills.").action(listCommand);
4086
4606
  program.command("status").description("Show local workspace sync status.").option("--target <target>", "claude | codex | cursor | gemini | opencode").action(statusCommand);
4087
4607
  program.command("mcp").description("Run the local MCP server over stdio.").action(mcpCommand);
4088
4608
  program.command("add <share-url-or-token>").description("Install a skill from a Floom share link.").option("--target <target>", "claude | codex | cursor | gemini | opencode").action((input, opts) => addCommand(input, opts));
4089
- program.command("rename-machine <label>").description('Set the friendly name for THIS machine (e.g. "Office Server", "Travel Mac").').action(renameMachineCommand);
4609
+ program.command("rename-machine <label>").description('Set the friendly name for this machine (e.g. "Office Server", "Travel Mac").').action(renameMachineCommand);
4090
4610
  async function main() {
4091
4611
  try {
4092
4612
  await program.parseAsync(process.argv);
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "2.0.5";
1
+ export const VERSION = "2.0.7";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floomhq/floom",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
4
4
  "description": "Floom CLI \u2014 one shared skill library, pulled into the AI agent you choose (Claude, Codex, Cursor, Gemini, OpenCode).",
5
5
  "license": "MIT",
6
6
  "homepage": "https://floom.dev",
@@ -41,6 +41,7 @@
41
41
  "zod": "^3.23.8"
42
42
  },
43
43
  "devDependencies": {
44
+ "@floom/shared": "workspace:*",
44
45
  "esbuild": "^0.27.7",
45
46
  "@types/node": "^22.0.0",
46
47
  "@types/prompts": "^2.4.9",