@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 +637 -117
- package/dist/version.js +1 -1
- package/package.json +2 -1
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
434
|
+
var cp3 = null;
|
|
435
435
|
if (typeof src === "number")
|
|
436
|
-
|
|
436
|
+
cp3 = src, src = function() {
|
|
437
437
|
return null;
|
|
438
438
|
};
|
|
439
|
-
while (
|
|
440
|
-
if (
|
|
441
|
-
dst(
|
|
442
|
-
else if (
|
|
443
|
-
dst(
|
|
444
|
-
else if (
|
|
445
|
-
dst(
|
|
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(
|
|
448
|
-
|
|
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
|
|
490
|
+
var cp3 = null;
|
|
491
491
|
if (typeof src === "number")
|
|
492
|
-
|
|
492
|
+
cp3 = src, src = function() {
|
|
493
493
|
return null;
|
|
494
494
|
};
|
|
495
|
-
while (
|
|
496
|
-
if (
|
|
497
|
-
dst(
|
|
495
|
+
while (cp3 !== null || (cp3 = src()) !== null) {
|
|
496
|
+
if (cp3 <= 65535)
|
|
497
|
+
dst(cp3);
|
|
498
498
|
else
|
|
499
|
-
|
|
500
|
-
|
|
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(
|
|
505
|
-
utfx2.encodeUTF8(
|
|
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(
|
|
510
|
-
utfx2.UTF8toUTF16(
|
|
509
|
+
utfx2.decodeUTF8(src, function(cp3) {
|
|
510
|
+
utfx2.UTF8toUTF16(cp3, dst);
|
|
511
511
|
});
|
|
512
512
|
};
|
|
513
|
-
utfx2.calculateCodePoint = function(
|
|
514
|
-
return
|
|
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
|
|
518
|
-
while ((
|
|
519
|
-
l += utfx2.calculateCodePoint(
|
|
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(
|
|
524
|
+
utfx2.UTF16toUTF8(src, function(cp3) {
|
|
525
525
|
++n;
|
|
526
|
-
l += utfx2.calculateCodePoint(
|
|
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
|
|
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(
|
|
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
|
|
2053
|
+
const configRoot = agentConfigRoot(target, homeDir);
|
|
2028
2054
|
try {
|
|
2029
|
-
const
|
|
2030
|
-
if (
|
|
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((
|
|
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
|
-
|
|
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) =>
|
|
2681
|
-
child.on("error", () =>
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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.
|
|
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((
|
|
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("
|
|
2923
|
-
log.info(`Open:
|
|
2924
|
-
log.info(`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
|
-
|
|
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
|
|
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
|
|
3257
|
-
if (
|
|
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(
|
|
3263
|
-
|
|
3264
|
-
for (const target2 of
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
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 (
|
|
3581
|
+
if (anyFailed2) process.exitCode = 1;
|
|
3280
3582
|
return;
|
|
3281
3583
|
}
|
|
3282
3584
|
const target = assertInstallTarget(options.target);
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
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
|
|
3343
|
-
|
|
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
|
|
3349
|
-
|
|
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
|
|
3356
|
-
if (
|
|
3357
|
-
|
|
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((
|
|
3678
|
+
if (results.some((result) => !result.ok)) process.exitCode = 1;
|
|
3360
3679
|
return;
|
|
3361
3680
|
}
|
|
3362
3681
|
const target = assertInstallTarget(options.target);
|
|
3363
|
-
const
|
|
3364
|
-
cleanup.trackDir(join5(
|
|
3365
|
-
|
|
3366
|
-
|
|
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
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
|
4287
|
+
import { join as join8 } from "node:path";
|
|
3780
4288
|
import { homedir as homedir4 } from "node:os";
|
|
3781
|
-
var MACHINE_FILE2 =
|
|
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
|
|
3821
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3986
|
-
const replacedDir =
|
|
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:
|
|
4499
|
+
const { stat: stat6 } = await import("node:fs/promises");
|
|
3992
4500
|
let existingDir = false;
|
|
3993
4501
|
try {
|
|
3994
|
-
await
|
|
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
|
|
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
|
|
4079
|
-
|
|
4080
|
-
program.
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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",
|