@floomhq/floom 2.0.5 → 2.0.6
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 +518 -108
- 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
|
};
|
|
@@ -2233,6 +2233,10 @@ var InviteCreateResponseSchema = z2.object({
|
|
|
2233
2233
|
invite: InviteSchema,
|
|
2234
2234
|
accept_url: UrlSchema
|
|
2235
2235
|
}).strict();
|
|
2236
|
+
var InviteLinkRotateResponseSchema = z2.object({
|
|
2237
|
+
invite: InviteSchema,
|
|
2238
|
+
accept_url: UrlSchema
|
|
2239
|
+
}).strict();
|
|
2236
2240
|
var InvitesListResponseSchema = z2.object({
|
|
2237
2241
|
invites: z2.array(InviteSchema),
|
|
2238
2242
|
total: z2.number().int().nonnegative()
|
|
@@ -2545,6 +2549,21 @@ var log = {
|
|
|
2545
2549
|
},
|
|
2546
2550
|
blank: () => {
|
|
2547
2551
|
process.stdout.write("\n");
|
|
2552
|
+
},
|
|
2553
|
+
// A runnable command rendered cyan+bold — the one thing the eye should catch.
|
|
2554
|
+
cmd: (s) => chalk.cyan.bold(s),
|
|
2555
|
+
// Codex-style next-step block: description line, then the command indented in cyan+bold.
|
|
2556
|
+
next: (description, command) => {
|
|
2557
|
+
process.stdout.write(`
|
|
2558
|
+
${description}
|
|
2559
|
+
${chalk.cyan.bold(command)}
|
|
2560
|
+
`);
|
|
2561
|
+
},
|
|
2562
|
+
// Aligned-column row for list/status tables.
|
|
2563
|
+
// col widths: [slug col, state col, rest]. Pass strings pre-padded or let row pad them.
|
|
2564
|
+
row: (cols, widths) => {
|
|
2565
|
+
const padded = cols.map((col, i) => widths[i] !== void 0 ? col.padEnd(widths[i]) : col);
|
|
2566
|
+
process.stdout.write(" " + padded.join(" ") + "\n");
|
|
2548
2567
|
}
|
|
2549
2568
|
};
|
|
2550
2569
|
|
|
@@ -2665,26 +2684,26 @@ import { spawn } from "node:child_process";
|
|
|
2665
2684
|
var CONFIG_DIR2 = join4(homedir3(), ".floom");
|
|
2666
2685
|
var MACHINE_FILE = join4(CONFIG_DIR2, "machine.json");
|
|
2667
2686
|
async function tryCommand(cmd, args) {
|
|
2668
|
-
return new Promise((
|
|
2687
|
+
return new Promise((resolve4) => {
|
|
2669
2688
|
let child;
|
|
2670
2689
|
try {
|
|
2671
2690
|
child = spawn(cmd, args, { stdio: ["ignore", "pipe", "ignore"] });
|
|
2672
2691
|
} catch {
|
|
2673
|
-
|
|
2692
|
+
resolve4(null);
|
|
2674
2693
|
return;
|
|
2675
2694
|
}
|
|
2676
2695
|
let out = "";
|
|
2677
2696
|
child.stdout?.on("data", (d) => {
|
|
2678
2697
|
out += d.toString();
|
|
2679
2698
|
});
|
|
2680
|
-
child.on("close", (code) =>
|
|
2681
|
-
child.on("error", () =>
|
|
2699
|
+
child.on("close", (code) => resolve4(code === 0 ? out.trim() : null));
|
|
2700
|
+
child.on("error", () => resolve4(null));
|
|
2682
2701
|
const timer = setTimeout(() => {
|
|
2683
2702
|
try {
|
|
2684
2703
|
child.kill();
|
|
2685
2704
|
} catch {
|
|
2686
2705
|
}
|
|
2687
|
-
|
|
2706
|
+
resolve4(null);
|
|
2688
2707
|
}, 800);
|
|
2689
2708
|
child.on("close", () => clearTimeout(timer));
|
|
2690
2709
|
});
|
|
@@ -2738,7 +2757,7 @@ async function getMachineIdentity() {
|
|
|
2738
2757
|
}
|
|
2739
2758
|
|
|
2740
2759
|
// src/version.ts
|
|
2741
|
-
var VERSION = "2.0.
|
|
2760
|
+
var VERSION = "2.0.6";
|
|
2742
2761
|
|
|
2743
2762
|
// src/api-client.ts
|
|
2744
2763
|
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
@@ -2908,8 +2927,33 @@ async function api(path, opts = {}) {
|
|
|
2908
2927
|
}
|
|
2909
2928
|
|
|
2910
2929
|
// src/commands/login.ts
|
|
2930
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
2931
|
+
import chalk2 from "chalk";
|
|
2932
|
+
function tryOpenBrowser(url) {
|
|
2933
|
+
if (process.env.FLOOM_NO_OPEN === "1") return;
|
|
2934
|
+
const platform2 = process.platform;
|
|
2935
|
+
let cmd;
|
|
2936
|
+
let args;
|
|
2937
|
+
if (platform2 === "darwin") {
|
|
2938
|
+
cmd = "open";
|
|
2939
|
+
args = [url];
|
|
2940
|
+
} else if (platform2 === "win32") {
|
|
2941
|
+
cmd = "cmd";
|
|
2942
|
+
args = ["/c", "start", "", url];
|
|
2943
|
+
} else {
|
|
2944
|
+
cmd = "xdg-open";
|
|
2945
|
+
args = [url];
|
|
2946
|
+
}
|
|
2947
|
+
try {
|
|
2948
|
+
const child = spawn2(cmd, args, { detached: true, stdio: "ignore" });
|
|
2949
|
+
child.on("error", () => {
|
|
2950
|
+
});
|
|
2951
|
+
child.unref();
|
|
2952
|
+
} catch {
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2911
2955
|
function sleep(ms) {
|
|
2912
|
-
return new Promise((
|
|
2956
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
2913
2957
|
}
|
|
2914
2958
|
async function loginCommand() {
|
|
2915
2959
|
const session = await api("/cli/device/start", {
|
|
@@ -2919,10 +2963,11 @@ async function loginCommand() {
|
|
|
2919
2963
|
client: `floom-cli/${VERSION}`
|
|
2920
2964
|
}
|
|
2921
2965
|
});
|
|
2922
|
-
log.heading("
|
|
2923
|
-
log.info(`Open:
|
|
2924
|
-
log.info(`Code:
|
|
2966
|
+
log.heading("Sign in to Floom");
|
|
2967
|
+
log.info(`Open: ${session.verification_uri}`);
|
|
2968
|
+
log.info(`Code: ${chalk2.bold(session.user_code)}`);
|
|
2925
2969
|
log.info("Waiting for browser approval. Press Ctrl+C to cancel.");
|
|
2970
|
+
tryOpenBrowser(session.verification_uri);
|
|
2926
2971
|
const deadline = new Date(session.expires_at).getTime();
|
|
2927
2972
|
const interval = Math.max(2, session.poll_interval_seconds) * 1e3;
|
|
2928
2973
|
while (Date.now() < deadline) {
|
|
@@ -2940,7 +2985,9 @@ async function loginCommand() {
|
|
|
2940
2985
|
email: token.user?.email ?? "unknown@example.com",
|
|
2941
2986
|
apiUrl: getApiUrl()
|
|
2942
2987
|
});
|
|
2943
|
-
|
|
2988
|
+
const emailDisplay = token.user?.email ? ` as ${token.user.email}` : "";
|
|
2989
|
+
log.ok(`Logged in to ${token.workspace?.name ?? "Floom"}${emailDisplay}.`);
|
|
2990
|
+
log.next("Next: pull your workspace skills into your AI agents.", "floom pull");
|
|
2944
2991
|
return;
|
|
2945
2992
|
}
|
|
2946
2993
|
} catch (error) {
|
|
@@ -2968,9 +3015,10 @@ import { z as z3 } from "zod";
|
|
|
2968
3015
|
// src/commands/sync.ts
|
|
2969
3016
|
import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
|
|
2970
3017
|
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";
|
|
3018
|
+
import { dirname, join as join5, resolve as resolve2, sep as sep3 } from "node:path";
|
|
2972
3019
|
import { createInterface } from "node:readline/promises";
|
|
2973
3020
|
import { ZodError } from "zod";
|
|
3021
|
+
import chalk3 from "chalk";
|
|
2974
3022
|
|
|
2975
3023
|
// src/lib/signals.ts
|
|
2976
3024
|
import { rmSync } from "node:fs";
|
|
@@ -3094,6 +3142,83 @@ async function fileExists(path) {
|
|
|
3094
3142
|
return false;
|
|
3095
3143
|
}
|
|
3096
3144
|
}
|
|
3145
|
+
async function dirExists(path) {
|
|
3146
|
+
try {
|
|
3147
|
+
return (await stat3(path)).isDirectory();
|
|
3148
|
+
} catch {
|
|
3149
|
+
return false;
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
async function resolvePullDirs(target, _cwd = process.cwd(), homeDir) {
|
|
3153
|
+
const primary = resolveInstallDir({ target, global: true, homeDir }).dir;
|
|
3154
|
+
return uniqueResolvedDirs([primary]);
|
|
3155
|
+
}
|
|
3156
|
+
async function hasStatusVisibleProjectLocalDir(dir) {
|
|
3157
|
+
if (await fileExists(manifestPath(dir))) return true;
|
|
3158
|
+
return await dirExists(dir) && await hasAnySkillSubdir(dir);
|
|
3159
|
+
}
|
|
3160
|
+
async function resolveProjectLocalStatusDirs(target, cwd = process.cwd()) {
|
|
3161
|
+
const projectLocal = presetDir(target, { cwd });
|
|
3162
|
+
if (!await hasStatusVisibleProjectLocalDir(projectLocal)) return [];
|
|
3163
|
+
return uniqueResolvedDirs([projectLocal]);
|
|
3164
|
+
}
|
|
3165
|
+
async function resolveStatusDirs(target, cwd = process.cwd(), homeDir) {
|
|
3166
|
+
const primary = resolveInstallDir({ target, global: true, homeDir }).dir;
|
|
3167
|
+
const projectLocalDirs = await resolveProjectLocalStatusDirs(target, cwd);
|
|
3168
|
+
return uniqueResolvedDirs([primary, ...projectLocalDirs]);
|
|
3169
|
+
}
|
|
3170
|
+
async function resolveAutoStatusTargets(cwd = process.cwd(), homeDir) {
|
|
3171
|
+
const detected = new Set(await detectInstalledTargets({ homeDir }));
|
|
3172
|
+
const resolved = [];
|
|
3173
|
+
for (const target of INSTALL_TARGETS) {
|
|
3174
|
+
if (detected.has(target)) {
|
|
3175
|
+
resolved.push({ target, dirs: await resolveStatusDirs(target, cwd, homeDir) });
|
|
3176
|
+
continue;
|
|
3177
|
+
}
|
|
3178
|
+
const projectLocalDirs = await resolveProjectLocalStatusDirs(target, cwd);
|
|
3179
|
+
if (projectLocalDirs.length > 0) resolved.push({ target, dirs: projectLocalDirs });
|
|
3180
|
+
}
|
|
3181
|
+
return resolved;
|
|
3182
|
+
}
|
|
3183
|
+
async function hasAnySkillSubdir(root) {
|
|
3184
|
+
let entries;
|
|
3185
|
+
try {
|
|
3186
|
+
entries = await readdir2(root, { withFileTypes: true });
|
|
3187
|
+
} catch {
|
|
3188
|
+
return false;
|
|
3189
|
+
}
|
|
3190
|
+
for (const entry of entries) {
|
|
3191
|
+
if (!entry.isDirectory()) continue;
|
|
3192
|
+
if (await fileExists(join5(root, entry.name, "SKILL.md"))) return true;
|
|
3193
|
+
}
|
|
3194
|
+
return false;
|
|
3195
|
+
}
|
|
3196
|
+
async function listSkillSubdirs(root) {
|
|
3197
|
+
let entries;
|
|
3198
|
+
try {
|
|
3199
|
+
entries = await readdir2(root, { withFileTypes: true });
|
|
3200
|
+
} catch {
|
|
3201
|
+
return [];
|
|
3202
|
+
}
|
|
3203
|
+
const matches = [];
|
|
3204
|
+
for (const entry of entries) {
|
|
3205
|
+
if (!entry.isDirectory()) continue;
|
|
3206
|
+
const candidate = join5(root, entry.name);
|
|
3207
|
+
if (await fileExists(join5(candidate, "SKILL.md"))) matches.push(candidate);
|
|
3208
|
+
}
|
|
3209
|
+
return matches.sort();
|
|
3210
|
+
}
|
|
3211
|
+
function uniqueResolvedDirs(dirs) {
|
|
3212
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3213
|
+
const unique = [];
|
|
3214
|
+
for (const dir of dirs) {
|
|
3215
|
+
const resolved = resolve2(dir);
|
|
3216
|
+
if (seen.has(resolved)) continue;
|
|
3217
|
+
seen.add(resolved);
|
|
3218
|
+
unique.push(resolved);
|
|
3219
|
+
}
|
|
3220
|
+
return unique;
|
|
3221
|
+
}
|
|
3097
3222
|
async function liveSkillHash(root, workspaceSlug, skill) {
|
|
3098
3223
|
const slug = safeSkillSlug(skill.slug);
|
|
3099
3224
|
const fileHashes = [];
|
|
@@ -3251,41 +3376,119 @@ function printNoDetectedTargets() {
|
|
|
3251
3376
|
log.info(` Or pick one explicitly: npx -y @floomhq/floom pull --target claude`);
|
|
3252
3377
|
log.info(` More help: https://floom.dev/docs#agents`);
|
|
3253
3378
|
}
|
|
3254
|
-
async function
|
|
3379
|
+
async function runStatusForDir(target, dir, deps) {
|
|
3380
|
+
const hasFloomManifest = await fileExists(manifestPath(dir));
|
|
3381
|
+
if (!hasFloomManifest) {
|
|
3382
|
+
const localSkillDirs = await listSkillSubdirs(dir);
|
|
3383
|
+
if (localSkillDirs.length > 0) {
|
|
3384
|
+
printRawLocalSkillsHint(target, dir, localSkillDirs);
|
|
3385
|
+
return { ok: true };
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3388
|
+
try {
|
|
3389
|
+
const status = await statusLibrary(target, { ...deps, installDir: dir });
|
|
3390
|
+
printFloomManagedStatus(target, dir, status);
|
|
3391
|
+
return { ok: true };
|
|
3392
|
+
} catch (error) {
|
|
3393
|
+
return { ok: false, error };
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
function shellSingleQuote(value) {
|
|
3397
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
3398
|
+
}
|
|
3399
|
+
function printRawLocalSkillsHint(target, dir, localSkillDirs) {
|
|
3400
|
+
log.heading(`Local skills found (${target})`);
|
|
3401
|
+
log.info(`dir ${dir}`);
|
|
3402
|
+
log.info(` Floom does not manage these yet. Push them to your library:`);
|
|
3403
|
+
for (const skillDir of localSkillDirs) {
|
|
3404
|
+
log.info(` - ${skillDir}`);
|
|
3405
|
+
}
|
|
3406
|
+
log.info(` Push a single skill: npx -y @floomhq/floom push ${shellSingleQuote(localSkillDirs[0])}`);
|
|
3407
|
+
log.info(` Push them all: npx -y @floomhq/floom push ${shellSingleQuote(dir)}`);
|
|
3408
|
+
}
|
|
3409
|
+
function stateLabel(state) {
|
|
3410
|
+
switch (state) {
|
|
3411
|
+
case "active":
|
|
3412
|
+
return chalk3.green("up to date");
|
|
3413
|
+
case "stale":
|
|
3414
|
+
return chalk3.yellow("needs pull");
|
|
3415
|
+
case "dirty":
|
|
3416
|
+
return chalk3.yellow("local edits");
|
|
3417
|
+
case "conflict":
|
|
3418
|
+
return chalk3.red("conflict");
|
|
3419
|
+
case "missing":
|
|
3420
|
+
return "not installed";
|
|
3421
|
+
case "unsupported_target":
|
|
3422
|
+
return "other agent";
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
function printFloomManagedStatus(target, dir, status) {
|
|
3426
|
+
log.info(`${status.workspaceName} \xB7 ${target} \xB7 ${dir}`);
|
|
3427
|
+
log.blank();
|
|
3428
|
+
const slugWidth = Math.max(...status.skills.map((s) => s.slug.length), 4);
|
|
3429
|
+
const labelWidth = 14;
|
|
3430
|
+
for (const line of status.skills) {
|
|
3431
|
+
const label = stateLabel(line.state);
|
|
3432
|
+
process.stdout.write(` ${line.slug.padEnd(slugWidth)} ${label.padEnd(labelWidth + (label.length - stripAnsiLength(label)))} ${line.version}
|
|
3433
|
+
`);
|
|
3434
|
+
}
|
|
3435
|
+
log.blank();
|
|
3436
|
+
const needsPull = status.skills.filter((s) => s.state === "stale").length;
|
|
3437
|
+
const localEdits = status.skills.filter((s) => s.state === "dirty").length;
|
|
3438
|
+
const conflicts = status.skills.filter((s) => s.state === "conflict").length;
|
|
3439
|
+
const parts = [`${status.skills.length} skill${status.skills.length !== 1 ? "s" : ""}`];
|
|
3440
|
+
if (needsPull > 0) parts.push(`${needsPull} needs pull`);
|
|
3441
|
+
if (localEdits > 0) parts.push(`${localEdits} with local edits`);
|
|
3442
|
+
if (conflicts > 0) parts.push(`${conflicts} conflict${conflicts !== 1 ? "s" : ""}`);
|
|
3443
|
+
log.info(parts.join(" \xB7 "));
|
|
3444
|
+
const showPull = needsPull > 0 || conflicts > 0;
|
|
3445
|
+
const showPush = localEdits > 0 || conflicts > 0;
|
|
3446
|
+
if (showPull || showPush) {
|
|
3447
|
+
log.blank();
|
|
3448
|
+
log.info("Next:");
|
|
3449
|
+
if (showPull) log.info(` ${log.cmd("floom pull")} update skills that are behind`);
|
|
3450
|
+
if (showPush) log.info(` ${log.cmd("floom push <dir>")} publish your local edits`);
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
var ANSI_STRIP_RE = /\x1b\[[0-9;]*m/g;
|
|
3454
|
+
function stripAnsiLength(s) {
|
|
3455
|
+
return s.replace(ANSI_STRIP_RE, "").length;
|
|
3456
|
+
}
|
|
3457
|
+
async function statusCommand(options, deps = {}) {
|
|
3255
3458
|
if (!options.target) {
|
|
3256
|
-
const
|
|
3257
|
-
if (
|
|
3459
|
+
const targetDirs = await resolveAutoStatusTargets();
|
|
3460
|
+
if (targetDirs.length === 0) {
|
|
3258
3461
|
printNoDetectedTargets();
|
|
3259
3462
|
process.exitCode = 1;
|
|
3260
3463
|
return;
|
|
3261
3464
|
}
|
|
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;
|
|
3465
|
+
printDetectedTargets(targetDirs.map(({ target: target2 }) => target2));
|
|
3466
|
+
let anyFailed2 = false;
|
|
3467
|
+
for (const { target: target2, dirs } of targetDirs) {
|
|
3468
|
+
for (const dir of dirs) {
|
|
3469
|
+
const result = await runStatusForDir(target2, dir, deps);
|
|
3470
|
+
if (!result.ok) {
|
|
3471
|
+
log.err(`${target2} ${dir} ${result.error.message}`);
|
|
3472
|
+
anyFailed2 = true;
|
|
3473
|
+
}
|
|
3275
3474
|
}
|
|
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
3475
|
}
|
|
3279
|
-
if (
|
|
3476
|
+
if (anyFailed2) process.exitCode = 1;
|
|
3280
3477
|
return;
|
|
3281
3478
|
}
|
|
3282
3479
|
const target = assertInstallTarget(options.target);
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3480
|
+
let anyFailed = false;
|
|
3481
|
+
for (const dir of await resolveStatusDirs(target)) {
|
|
3482
|
+
const result = await runStatusForDir(target, dir, deps);
|
|
3483
|
+
if (!result.ok) {
|
|
3484
|
+
log.err(`${target} ${dir} ${result.error.message}`);
|
|
3485
|
+
anyFailed = true;
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
if (anyFailed) process.exitCode = 1;
|
|
3286
3489
|
}
|
|
3287
3490
|
async function statusLibrary(target, deps = {}) {
|
|
3288
|
-
const resolved = resolveInstallDir({ target, global: true });
|
|
3491
|
+
const resolved = { dir: deps.installDir ?? resolveInstallDir({ target, global: true }).dir };
|
|
3289
3492
|
const remote = await (deps.fetchLibrary ?? fetchLibrary)(target, "status");
|
|
3290
3493
|
const manifest = await readManifest(resolved.dir);
|
|
3291
3494
|
if (!manifest) {
|
|
@@ -3323,7 +3526,7 @@ async function statusLibrary(target, deps = {}) {
|
|
|
3323
3526
|
}
|
|
3324
3527
|
return { workspaceName: remote.workspace.name, skills };
|
|
3325
3528
|
}
|
|
3326
|
-
async function pullCommand(options) {
|
|
3529
|
+
async function pullCommand(options, deps = {}) {
|
|
3327
3530
|
const cleanup = installCancellationHandler();
|
|
3328
3531
|
try {
|
|
3329
3532
|
if (!options.target) {
|
|
@@ -3338,39 +3541,50 @@ async function pullCommand(options) {
|
|
|
3338
3541
|
log.info("Cancelled.");
|
|
3339
3542
|
return;
|
|
3340
3543
|
}
|
|
3544
|
+
const dirsByTarget = /* @__PURE__ */ new Map();
|
|
3341
3545
|
for (const target2 of targets) {
|
|
3342
|
-
const
|
|
3343
|
-
|
|
3546
|
+
const dirs2 = await resolvePullDirs(target2);
|
|
3547
|
+
dirsByTarget.set(target2, dirs2);
|
|
3548
|
+
for (const dir of dirs2) cleanup.trackDir(join5(dir, ".floom", "tmp"));
|
|
3344
3549
|
}
|
|
3345
3550
|
const results = [];
|
|
3346
3551
|
for (const target2 of targets) {
|
|
3347
3552
|
try {
|
|
3348
|
-
const
|
|
3349
|
-
|
|
3553
|
+
const dirs2 = dirsByTarget.get(target2) ?? [];
|
|
3554
|
+
let skillCount2 = 0;
|
|
3555
|
+
for (const dir of dirs2) {
|
|
3556
|
+
const result = await pullLibrary(target2, { ...deps, installDir: dir });
|
|
3557
|
+
skillCount2 = result.skillCount;
|
|
3558
|
+
}
|
|
3559
|
+
results.push({ target: target2, ok: true, skillCount: skillCount2, dirs: dirs2 });
|
|
3350
3560
|
} catch (error) {
|
|
3351
3561
|
results.push({ target: target2, ok: false, error });
|
|
3352
3562
|
}
|
|
3353
3563
|
}
|
|
3354
3564
|
log.heading("Pull summary:");
|
|
3355
|
-
for (const
|
|
3356
|
-
if (
|
|
3357
|
-
else log.err(`${
|
|
3565
|
+
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}`);
|
|
3358
3568
|
}
|
|
3359
|
-
if (results.some((
|
|
3569
|
+
if (results.some((result) => !result.ok)) process.exitCode = 1;
|
|
3360
3570
|
return;
|
|
3361
3571
|
}
|
|
3362
3572
|
const target = assertInstallTarget(options.target);
|
|
3363
|
-
const
|
|
3364
|
-
cleanup.trackDir(join5(
|
|
3365
|
-
|
|
3366
|
-
|
|
3573
|
+
const dirs = await resolvePullDirs(target);
|
|
3574
|
+
for (const dir of dirs) cleanup.trackDir(join5(dir, ".floom", "tmp"));
|
|
3575
|
+
let skillCount = 0;
|
|
3576
|
+
for (const dir of dirs) {
|
|
3577
|
+
const result = await pullLibrary(target, { ...deps, installDir: dir });
|
|
3578
|
+
skillCount = result.skillCount;
|
|
3579
|
+
}
|
|
3580
|
+
log.ok(`Pulled ${skillCount} skills into ${target} (${dirs.join(", ")}).`);
|
|
3367
3581
|
log.info(`This syncs ${target} only. For another agent: npx -y @floomhq/floom pull --target <claude|codex|cursor|gemini|opencode>`);
|
|
3368
3582
|
} finally {
|
|
3369
3583
|
cleanup.dispose();
|
|
3370
3584
|
}
|
|
3371
3585
|
}
|
|
3372
3586
|
async function pullLibrary(target, deps = {}) {
|
|
3373
|
-
const resolved = resolveInstallDir({ target, global: true });
|
|
3587
|
+
const resolved = { dir: deps.installDir ?? resolveInstallDir({ target, global: true }).dir };
|
|
3374
3588
|
const remote = await (deps.fetchLibrary ?? fetchLibrary)(target, "pull");
|
|
3375
3589
|
for (const skill of remote.skills) safeSkillSlug(skill.slug);
|
|
3376
3590
|
const manifest = await readManifest(resolved.dir);
|
|
@@ -3630,13 +3844,19 @@ function createMcpServer(deps = {}) {
|
|
|
3630
3844
|
return server;
|
|
3631
3845
|
}
|
|
3632
3846
|
async function mcpCommand() {
|
|
3847
|
+
if (process.stderr.isTTY || process.stdin.isTTY) {
|
|
3848
|
+
process.stderr.write("Floom MCP server running (stdio).\n");
|
|
3849
|
+
process.stderr.write("This is launched by your AI agent, not run by hand.\n");
|
|
3850
|
+
process.stderr.write("Add it to Claude: claude mcp add floom -- npx -y @floomhq/floom mcp\n");
|
|
3851
|
+
process.stderr.write("Ctrl+C to stop.\n");
|
|
3852
|
+
}
|
|
3633
3853
|
const server = createMcpServer();
|
|
3634
3854
|
const transport = new StdioServerTransport();
|
|
3635
3855
|
await server.connect(transport);
|
|
3636
3856
|
}
|
|
3637
3857
|
|
|
3638
3858
|
// src/commands/push.ts
|
|
3639
|
-
import { basename, join as join6, resolve as
|
|
3859
|
+
import { basename, join as join6, resolve as resolve3 } from "node:path";
|
|
3640
3860
|
import { readdir as readdir3, readFile as readFile6, stat as stat4 } from "node:fs/promises";
|
|
3641
3861
|
function parseConcurrency(value) {
|
|
3642
3862
|
const raw = value ?? 6;
|
|
@@ -3701,7 +3921,7 @@ async function runBounded(items, concurrency, worker) {
|
|
|
3701
3921
|
async function pushCommand(dir = ".", options = {}, deps = {}) {
|
|
3702
3922
|
const cleanup = installCancellationHandler();
|
|
3703
3923
|
try {
|
|
3704
|
-
const root =
|
|
3924
|
+
const root = resolve3(dir);
|
|
3705
3925
|
const dirStat = await stat4(root).catch(() => null);
|
|
3706
3926
|
if (!dirStat || !dirStat.isDirectory()) {
|
|
3707
3927
|
throw new Error(
|
|
@@ -3713,6 +3933,7 @@ async function pushCommand(dir = ".", options = {}, deps = {}) {
|
|
|
3713
3933
|
if (await hasSkillMarkdown(root)) {
|
|
3714
3934
|
const result = await pushOneSkill(root, pushApi);
|
|
3715
3935
|
log.ok(`Pushed ${result.skill.slug} ${result.skill.latest.display}.`);
|
|
3936
|
+
log.next("Next: pull this skill into your AI agents.", "floom pull");
|
|
3716
3937
|
return;
|
|
3717
3938
|
}
|
|
3718
3939
|
const skillDirs = await findImmediateSkillDirs(root);
|
|
@@ -3729,7 +3950,7 @@ async function pushCommand(dir = ".", options = {}, deps = {}) {
|
|
|
3729
3950
|
try {
|
|
3730
3951
|
const result = await pushOneSkill(skillDir, pushApi);
|
|
3731
3952
|
pushed += 1;
|
|
3732
|
-
log.
|
|
3953
|
+
log.ok(`${result.skill.slug} ${result.skill.latest.display}`);
|
|
3733
3954
|
} catch (error) {
|
|
3734
3955
|
errors.push({ slug, message: error.message });
|
|
3735
3956
|
}
|
|
@@ -3766,19 +3987,196 @@ async function deleteCommand(slug, opts = {}, deps = defaultDeps) {
|
|
|
3766
3987
|
async function listCommand() {
|
|
3767
3988
|
const result = await api("/skills", { authRequired: true });
|
|
3768
3989
|
if (result.total === 0) {
|
|
3769
|
-
log.info("No skills in this workspace.");
|
|
3990
|
+
log.info("No skills in this workspace yet.");
|
|
3991
|
+
log.next("Next: publish your first skill.", "floom push ./path/to/skill-folder");
|
|
3770
3992
|
return;
|
|
3771
3993
|
}
|
|
3994
|
+
const slugWidth = Math.max(...result.skills.map((s) => s.slug.length), 4);
|
|
3995
|
+
const verWidth = Math.max(...result.skills.map((s) => s.latest_version.display.length), 3);
|
|
3772
3996
|
for (const skill of result.skills) {
|
|
3773
|
-
log.
|
|
3997
|
+
log.row([skill.slug, skill.latest_version.display, skill.title], [slugWidth, verWidth, 0]);
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
4000
|
+
|
|
4001
|
+
// src/commands/sync-command.ts
|
|
4002
|
+
import { cp as cp2, mkdtemp, readdir as readdir4, rm as rm2, stat as stat5 } from "node:fs/promises";
|
|
4003
|
+
import { tmpdir } from "node:os";
|
|
4004
|
+
import { join as join7 } from "node:path";
|
|
4005
|
+
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
4006
|
+
async function hasSkillMarkdown2(dir) {
|
|
4007
|
+
try {
|
|
4008
|
+
const skillMd = await stat5(join7(dir, "SKILL.md"));
|
|
4009
|
+
return skillMd.isFile();
|
|
4010
|
+
} catch {
|
|
4011
|
+
return false;
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
async function findImmediateSkillDirs2(root) {
|
|
4015
|
+
let entries;
|
|
4016
|
+
try {
|
|
4017
|
+
entries = await readdir4(root, { withFileTypes: true });
|
|
4018
|
+
} catch (error) {
|
|
4019
|
+
const err = error;
|
|
4020
|
+
if (err.code === "ENOENT") return [];
|
|
4021
|
+
throw error;
|
|
4022
|
+
}
|
|
4023
|
+
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));
|
|
4024
|
+
const checks = await Promise.all(dirs.map(async (entry) => await hasSkillMarkdown2(entry.dir) ? entry : null));
|
|
4025
|
+
return checks.filter((entry) => Boolean(entry));
|
|
4026
|
+
}
|
|
4027
|
+
function formatSlugs(slugs) {
|
|
4028
|
+
return slugs.length > 0 ? slugs.join(", ") : "none";
|
|
4029
|
+
}
|
|
4030
|
+
function printPreview(plan) {
|
|
4031
|
+
log.info(
|
|
4032
|
+
`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))}).`
|
|
4033
|
+
);
|
|
4034
|
+
if (plan.conflicts.length > 0) {
|
|
4035
|
+
const count = plan.conflicts.length;
|
|
4036
|
+
const noun = count === 1 ? "skill changed" : "skills changed";
|
|
4037
|
+
log.err(`${count} ${noun} both locally and on the server: Floom won't guess which wins.`);
|
|
4038
|
+
for (const skill of plan.conflicts) {
|
|
4039
|
+
log.err(` - ${skill.slug} (${skill.version})`);
|
|
4040
|
+
log.err(` Keep the server version: floom pull --target ${plan.target}`);
|
|
4041
|
+
log.err(` Keep your local version: floom push <${skill.slug}-dir>`);
|
|
4042
|
+
}
|
|
4043
|
+
log.err("Your local copy is always backed up to .floom/backups/ first.");
|
|
4044
|
+
log.err("More: https://floom.dev/docs#conflicts");
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
async function defaultConfirmProceed() {
|
|
4048
|
+
if (!process.stdin.isTTY) {
|
|
4049
|
+
log.err("Aborted: confirmation is required in non-interactive mode. Re-run with --yes to proceed.");
|
|
4050
|
+
return false;
|
|
4051
|
+
}
|
|
4052
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
4053
|
+
try {
|
|
4054
|
+
const answer = (await rl.question("Proceed? [Y/n] ")).trim().toLowerCase();
|
|
4055
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
4056
|
+
} finally {
|
|
4057
|
+
rl.close();
|
|
4058
|
+
}
|
|
4059
|
+
}
|
|
4060
|
+
async function resolveTarget(options, deps) {
|
|
4061
|
+
if (options.target) return assertInstallTarget(options.target);
|
|
4062
|
+
const targets = await (deps.detectInstalledTargets ?? detectInstalledTargets)();
|
|
4063
|
+
if (targets.length === 0) {
|
|
4064
|
+
log.err("No AI agent recognized on this machine. Re-run with --target <claude|codex|cursor|gemini|opencode>.");
|
|
4065
|
+
process.exitCode = 1;
|
|
4066
|
+
return null;
|
|
4067
|
+
}
|
|
4068
|
+
if (targets.length > 1) {
|
|
4069
|
+
log.err(`Multiple AI agents detected (${targets.join(", ")}). Re-run with --target <agent>.`);
|
|
4070
|
+
process.exitCode = 1;
|
|
4071
|
+
return null;
|
|
4072
|
+
}
|
|
4073
|
+
return targets[0];
|
|
4074
|
+
}
|
|
4075
|
+
async function createPushSnapshots(pushes) {
|
|
4076
|
+
const root = await mkdtemp(join7(tmpdir(), "floom-sync-push-"));
|
|
4077
|
+
const dirs = [];
|
|
4078
|
+
try {
|
|
4079
|
+
for (const push of pushes) {
|
|
4080
|
+
const dest = join7(root, push.slug);
|
|
4081
|
+
await cp2(push.dir, dest, { recursive: true });
|
|
4082
|
+
dirs.push({ slug: push.slug, dir: dest });
|
|
4083
|
+
}
|
|
4084
|
+
return { root, dirs };
|
|
4085
|
+
} catch (error) {
|
|
4086
|
+
await rm2(root, { recursive: true, force: true });
|
|
4087
|
+
throw error;
|
|
4088
|
+
}
|
|
4089
|
+
}
|
|
4090
|
+
async function buildPlan(options, deps) {
|
|
4091
|
+
const target = await resolveTarget(options, deps);
|
|
4092
|
+
if (!target) return null;
|
|
4093
|
+
const status = await (deps.statusLibrary ?? statusLibrary)(target);
|
|
4094
|
+
const dirtySlugs = new Set(status.skills.filter((skill) => skill.state === "dirty").map((skill) => skill.slug));
|
|
4095
|
+
const installDir = resolveInstallDir({ target, global: true }).dir;
|
|
4096
|
+
const localSkillDirs = await findImmediateSkillDirs2(installDir);
|
|
4097
|
+
return {
|
|
4098
|
+
target,
|
|
4099
|
+
pull: status.skills.filter((skill) => skill.state === "missing" || skill.state === "stale"),
|
|
4100
|
+
push: localSkillDirs.filter((entry) => dirtySlugs.has(entry.slug)),
|
|
4101
|
+
conflicts: status.skills.filter((skill) => skill.state === "conflict")
|
|
4102
|
+
};
|
|
4103
|
+
}
|
|
4104
|
+
async function syncCommand(options = {}, deps = {}) {
|
|
4105
|
+
const plan = await buildPlan(options, deps);
|
|
4106
|
+
if (!plan) return;
|
|
4107
|
+
if (plan.pull.length === 0 && plan.push.length === 0 && plan.conflicts.length === 0) {
|
|
4108
|
+
log.info("Already in sync.");
|
|
4109
|
+
return;
|
|
4110
|
+
}
|
|
4111
|
+
printPreview(plan);
|
|
4112
|
+
if (plan.conflicts.length > 0) {
|
|
4113
|
+
process.exitCode = 1;
|
|
4114
|
+
return;
|
|
4115
|
+
}
|
|
4116
|
+
if (plan.pull.length === 0 && plan.push.length === 0) return;
|
|
4117
|
+
if (options.yes) {
|
|
4118
|
+
log.info("Proceeding (--yes).");
|
|
4119
|
+
} else if (!await (deps.confirmProceed ?? defaultConfirmProceed)()) {
|
|
4120
|
+
log.info("Aborted.");
|
|
4121
|
+
return;
|
|
4122
|
+
}
|
|
4123
|
+
const snapshots = await createPushSnapshots(plan.push);
|
|
4124
|
+
const pushFailures = [];
|
|
4125
|
+
let pushed = 0;
|
|
4126
|
+
let preserveSnapshotsForRecovery = false;
|
|
4127
|
+
try {
|
|
4128
|
+
try {
|
|
4129
|
+
await (deps.pullLibrary ?? pullLibrary)(plan.target);
|
|
4130
|
+
} catch (error) {
|
|
4131
|
+
log.err(`Pull failed: ${error.message}`);
|
|
4132
|
+
process.exitCode = 1;
|
|
4133
|
+
return;
|
|
4134
|
+
}
|
|
4135
|
+
for (const snapshot of snapshots.dirs) {
|
|
4136
|
+
try {
|
|
4137
|
+
await (deps.pushSkill ?? ((dir) => pushCommand(dir)))(snapshot.dir);
|
|
4138
|
+
pushed += 1;
|
|
4139
|
+
} catch (error) {
|
|
4140
|
+
pushFailures.push({
|
|
4141
|
+
slug: snapshot.slug,
|
|
4142
|
+
message: error.message,
|
|
4143
|
+
snapshotDir: snapshot.dir
|
|
4144
|
+
});
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
} finally {
|
|
4148
|
+
if (pushFailures.length === 0) {
|
|
4149
|
+
await rm2(snapshots.root, { recursive: true, force: true });
|
|
4150
|
+
} else {
|
|
4151
|
+
preserveSnapshotsForRecovery = true;
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
if (pushFailures.length > 0) {
|
|
4155
|
+
log.err(`Push failed for ${pushFailures.length} of ${plan.push.length} skill(s).`);
|
|
4156
|
+
log.err("Your local edits are preserved in two places (nothing is lost):");
|
|
4157
|
+
log.err(` 1. Snapshot taken before pull: ${snapshots.root}`);
|
|
4158
|
+
log.err(` 2. Pull backup of pre-pull dir: ${plan.target} \u2192 .floom/backups/<latest>/`);
|
|
4159
|
+
log.err("");
|
|
4160
|
+
log.err("Failed skills:");
|
|
4161
|
+
for (const failure of pushFailures) {
|
|
4162
|
+
log.err(` - ${failure.slug}: ${failure.message}`);
|
|
4163
|
+
log.err(` snapshot at: ${failure.snapshotDir}`);
|
|
4164
|
+
}
|
|
4165
|
+
log.err("");
|
|
4166
|
+
log.err(`Re-run \`floom sync --target ${plan.target}\` after the network recovers.`);
|
|
4167
|
+
process.exitCode = 1;
|
|
4168
|
+
}
|
|
4169
|
+
void preserveSnapshotsForRecovery;
|
|
4170
|
+
if (pushFailures.length === 0) {
|
|
4171
|
+
log.ok(`Sync complete. Pulled ${plan.pull.length} skills. Pushed ${pushed}/${plan.push.length} skills.`);
|
|
3774
4172
|
}
|
|
3775
4173
|
}
|
|
3776
4174
|
|
|
3777
4175
|
// src/commands/rename-machine.ts
|
|
3778
4176
|
import { readFile as readFile7, writeFile as writeFile4 } from "node:fs/promises";
|
|
3779
|
-
import { join as
|
|
4177
|
+
import { join as join8 } from "node:path";
|
|
3780
4178
|
import { homedir as homedir4 } from "node:os";
|
|
3781
|
-
var MACHINE_FILE2 =
|
|
4179
|
+
var MACHINE_FILE2 = join8(homedir4(), ".floom", "machine.json");
|
|
3782
4180
|
async function renameMachineCommand(newLabel, _opts) {
|
|
3783
4181
|
const trimmed = newLabel.trim().slice(0, 80);
|
|
3784
4182
|
if (!trimmed) {
|
|
@@ -3817,8 +4215,8 @@ async function renameMachineCommand(newLabel, _opts) {
|
|
|
3817
4215
|
}
|
|
3818
4216
|
|
|
3819
4217
|
// src/commands/add.ts
|
|
3820
|
-
import { mkdir as mkdir5, rm as
|
|
3821
|
-
import { join as
|
|
4218
|
+
import { mkdir as mkdir5, rm as rm3, writeFile as writeFile5 } from "node:fs/promises";
|
|
4219
|
+
import { join as join9 } from "node:path";
|
|
3822
4220
|
var TOKEN_RE = /^fls_[A-Za-z0-9_-]{32,}$/;
|
|
3823
4221
|
function parseToken(input) {
|
|
3824
4222
|
const trimmed = input.trim();
|
|
@@ -3971,27 +4369,27 @@ async function addCommand(input, opts = {}) {
|
|
|
3971
4369
|
safeRemotePath2(file.path);
|
|
3972
4370
|
}
|
|
3973
4371
|
const cleanup = installCancellationHandler();
|
|
3974
|
-
const tempDir =
|
|
4372
|
+
const tempDir = join9(resolved.dir, ".floom", "tmp", `${slug}-add-${Date.now()}`);
|
|
3975
4373
|
cleanup.trackDir(tempDir);
|
|
3976
4374
|
try {
|
|
3977
4375
|
await mkdir5(tempDir, { recursive: true });
|
|
3978
4376
|
for (const file of shareData.file_contents) {
|
|
3979
4377
|
const safePath = safeRemotePath2(file.path);
|
|
3980
|
-
const dest =
|
|
4378
|
+
const dest = join9(tempDir, ...safePath.split("/"));
|
|
3981
4379
|
const destDir = dest.substring(0, dest.lastIndexOf("/"));
|
|
3982
4380
|
if (destDir !== tempDir) await mkdir5(destDir, { recursive: true });
|
|
3983
4381
|
await writeFile5(dest, bytesForShareFile(file));
|
|
3984
4382
|
}
|
|
3985
|
-
const finalDir =
|
|
3986
|
-
const replacedDir =
|
|
4383
|
+
const finalDir = join9(resolved.dir, slug);
|
|
4384
|
+
const replacedDir = join9(resolved.dir, ".floom", "tmp", `${slug}-previous-${Date.now()}`);
|
|
3987
4385
|
let movedExisting = false;
|
|
3988
4386
|
try {
|
|
3989
4387
|
const { rename: rename2, rm: rmFs } = await import("node:fs/promises");
|
|
3990
4388
|
await mkdir5(resolved.dir, { recursive: true });
|
|
3991
|
-
const { stat:
|
|
4389
|
+
const { stat: stat6 } = await import("node:fs/promises");
|
|
3992
4390
|
let existingDir = false;
|
|
3993
4391
|
try {
|
|
3994
|
-
await
|
|
4392
|
+
await stat6(finalDir);
|
|
3995
4393
|
existingDir = true;
|
|
3996
4394
|
} catch {
|
|
3997
4395
|
}
|
|
@@ -4025,7 +4423,7 @@ async function addCommand(input, opts = {}) {
|
|
|
4025
4423
|
} finally {
|
|
4026
4424
|
cleanup.dispose();
|
|
4027
4425
|
try {
|
|
4028
|
-
await
|
|
4426
|
+
await rm3(tempDir, { recursive: true, force: true });
|
|
4029
4427
|
} catch {
|
|
4030
4428
|
}
|
|
4031
4429
|
}
|
|
@@ -4075,18 +4473,30 @@ async function whoamiCommand() {
|
|
|
4075
4473
|
}
|
|
4076
4474
|
}
|
|
4077
4475
|
var program = new Command();
|
|
4078
|
-
program.name("floom").description("Floom
|
|
4079
|
-
|
|
4080
|
-
program.
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4476
|
+
program.name("floom").description("Floom: one shared skill library, synced across every AI agent.").version(VERSION).addHelpCommand(false).hook("preAction", () => {
|
|
4477
|
+
});
|
|
4478
|
+
program.action(() => {
|
|
4479
|
+
log.info("Floom: one shared skill library, synced across every AI agent.");
|
|
4480
|
+
log.blank();
|
|
4481
|
+
log.info("New here?");
|
|
4482
|
+
log.info(` 1. ${log.cmd("floom login")} sign in`);
|
|
4483
|
+
log.info(` 2. ${log.cmd("floom pull")} get your team's skills (or ${log.cmd("floom push <dir>")} to publish)`);
|
|
4484
|
+
log.info(` 3. ${log.cmd("floom status")} see what's synced`);
|
|
4485
|
+
log.blank();
|
|
4486
|
+
log.info(`All commands: ${log.cmd("floom --help")}`);
|
|
4487
|
+
});
|
|
4488
|
+
program.command("login").description("Sign in via browser.").action(loginCommand);
|
|
4489
|
+
program.command("logout").description("Sign out and clear local auth.").action(logoutCommand);
|
|
4490
|
+
program.command("whoami").description("Show who you are signed in as.").action(whoamiCommand);
|
|
4491
|
+
program.command("push [dir]").description("Publish a skill folder to your workspace.").option("--concurrency <n>", "Bulk push concurrency, 1-16", "6").action(pushCommand);
|
|
4492
|
+
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);
|
|
4494
|
+
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
4495
|
program.command("list").description("List workspace skills.").action(listCommand);
|
|
4086
4496
|
program.command("status").description("Show local workspace sync status.").option("--target <target>", "claude | codex | cursor | gemini | opencode").action(statusCommand);
|
|
4087
4497
|
program.command("mcp").description("Run the local MCP server over stdio.").action(mcpCommand);
|
|
4088
4498
|
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
|
|
4499
|
+
program.command("rename-machine <label>").description('Set the friendly name for this machine (e.g. "Office Server", "Travel Mac").').action(renameMachineCommand);
|
|
4090
4500
|
async function main() {
|
|
4091
4501
|
try {
|
|
4092
4502
|
await program.parseAsync(process.argv);
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "2.0.
|
|
1
|
+
export const VERSION = "2.0.6";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@floomhq/floom",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.6",
|
|
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",
|