@floomhq/floom 2.0.6 → 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
@@ -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
  }
@@ -2675,7 +2716,7 @@ function normalizeApiUrl(apiUrl) {
2675
2716
  }
2676
2717
 
2677
2718
  // src/lib/machine.ts
2678
- import { hostname, platform, type, release } from "node:os";
2719
+ import { hostname, platform as platform2, type, release } from "node:os";
2679
2720
  import { join as join4 } from "node:path";
2680
2721
  import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
2681
2722
  import { homedir as homedir3 } from "node:os";
@@ -2709,7 +2750,7 @@ async function tryCommand(cmd, args) {
2709
2750
  });
2710
2751
  }
2711
2752
  async function nativeDeviceName() {
2712
- const p = platform();
2753
+ const p = platform2();
2713
2754
  if (p === "darwin") {
2714
2755
  return tryCommand("scutil", ["--get", "ComputerName"]);
2715
2756
  }
@@ -2722,7 +2763,7 @@ async function defaultLabel() {
2722
2763
  const native = await nativeDeviceName();
2723
2764
  const host = (hostname() || "").trim();
2724
2765
  const base = native || host || `${type()}`.slice(0, 40);
2725
- const p = platform();
2766
+ const p = platform2();
2726
2767
  const os = p === "darwin" ? "macOS" : p === "linux" ? "Linux" : p === "win32" ? "Windows" : type();
2727
2768
  const label = base.toLowerCase().includes(os.toLowerCase()) ? base : `${base} \xB7 ${os}`;
2728
2769
  return label.slice(0, 80);
@@ -2931,13 +2972,13 @@ import { spawn as spawn2 } from "node:child_process";
2931
2972
  import chalk2 from "chalk";
2932
2973
  function tryOpenBrowser(url) {
2933
2974
  if (process.env.FLOOM_NO_OPEN === "1") return;
2934
- const platform2 = process.platform;
2975
+ const platform3 = process.platform;
2935
2976
  let cmd;
2936
2977
  let args;
2937
- if (platform2 === "darwin") {
2978
+ if (platform3 === "darwin") {
2938
2979
  cmd = "open";
2939
2980
  args = [url];
2940
- } else if (platform2 === "win32") {
2981
+ } else if (platform3 === "win32") {
2941
2982
  cmd = "cmd";
2942
2983
  args = ["/c", "start", "", url];
2943
2984
  } else {
@@ -3016,6 +3057,7 @@ import { z as z3 } from "zod";
3016
3057
  import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
3017
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";
3018
3059
  import { dirname, join as join5, resolve as resolve2, sep as sep3 } from "node:path";
3060
+ import { homedir as osHomedir } from "node:os";
3019
3061
  import { createInterface } from "node:readline/promises";
3020
3062
  import { ZodError } from "zod";
3021
3063
  import chalk3 from "chalk";
@@ -3149,9 +3191,72 @@ async function dirExists(path) {
3149
3191
  return false;
3150
3192
  }
3151
3193
  }
3152
- async function resolvePullDirs(target, _cwd = process.cwd(), homeDir) {
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) {
3153
3251
  const primary = resolveInstallDir({ target, global: true, homeDir }).dir;
3154
- return uniqueResolvedDirs([primary]);
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]);
3155
3260
  }
3156
3261
  async function hasStatusVisibleProjectLocalDir(dir) {
3157
3262
  if (await fileExists(manifestPath(dir))) return true;
@@ -3543,7 +3648,7 @@ async function pullCommand(options, deps = {}) {
3543
3648
  }
3544
3649
  const dirsByTarget = /* @__PURE__ */ new Map();
3545
3650
  for (const target2 of targets) {
3546
- const dirs2 = await resolvePullDirs(target2);
3651
+ const dirs2 = await resolvePullDirs(target2, process.cwd(), void 0, options.globalOnly);
3547
3652
  dirsByTarget.set(target2, dirs2);
3548
3653
  for (const dir of dirs2) cleanup.trackDir(join5(dir, ".floom", "tmp"));
3549
3654
  }
@@ -3563,21 +3668,26 @@ async function pullCommand(options, deps = {}) {
3563
3668
  }
3564
3669
  log.heading("Pull summary:");
3565
3670
  for (const result of results) {
3566
- if (result.ok) log.ok(`${result.target} ${result.skillCount} skills ${result.dirs.join(", ")}`);
3567
- else log.err(`${result.target} ${result.error.message}`);
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
+ }
3568
3677
  }
3569
3678
  if (results.some((result) => !result.ok)) process.exitCode = 1;
3570
3679
  return;
3571
3680
  }
3572
3681
  const target = assertInstallTarget(options.target);
3573
- const dirs = await resolvePullDirs(target);
3682
+ const dirs = await resolvePullDirs(target, process.cwd(), void 0, options.globalOnly);
3574
3683
  for (const dir of dirs) cleanup.trackDir(join5(dir, ".floom", "tmp"));
3575
3684
  let skillCount = 0;
3576
3685
  for (const dir of dirs) {
3577
3686
  const result = await pullLibrary(target, { ...deps, installDir: dir });
3578
3687
  skillCount = result.skillCount;
3579
3688
  }
3580
- log.ok(`Pulled ${skillCount} skills into ${target} (${dirs.join(", ")}).`);
3689
+ log.ok(`Pulled ${skillCount} skills into ${target}.`);
3690
+ for (const dir of dirs) log.info(` \u2192 ${dir}`);
3581
3691
  log.info(`This syncs ${target} only. For another agent: npx -y @floomhq/floom pull --target <claude|codex|cursor|gemini|opencode>`);
3582
3692
  } finally {
3583
3693
  cleanup.dispose();
@@ -4490,7 +4600,7 @@ program.command("logout").description("Sign out and clear local auth.").action(l
4490
4600
  program.command("whoami").description("Show who you are signed in as.").action(whoamiCommand);
4491
4601
  program.command("push [dir]").description("Publish a skill folder to your workspace.").option("--concurrency <n>", "Bulk push concurrency, 1-16", "6").action(pushCommand);
4492
4602
  program.command("delete <slug>").description("Delete a skill from your workspace.").option("--yes", "Skip confirmation").action((slug, opts) => deleteCommand(slug, opts));
4493
- program.command("pull").description("Pull the workspace library into your AI agents.").option("--target <target>", "claude | codex | cursor | gemini | opencode").action(pullCommand);
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 }));
4494
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);
4495
4605
  program.command("list").description("List workspace skills.").action(listCommand);
4496
4606
  program.command("status").description("Show local workspace sync status.").option("--target <target>", "claude | codex | cursor | gemini | opencode").action(statusCommand);
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "2.0.6";
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.6",
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",