@floomhq/floom 2.0.4 → 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 +538 -112
- 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()
|
|
@@ -2503,6 +2507,20 @@ var ActivityFeedResponseSchema = z2.object({
|
|
|
2503
2507
|
events: z2.array(ActivityEventSchema),
|
|
2504
2508
|
total: z2.number().int().nonnegative()
|
|
2505
2509
|
}).strict();
|
|
2510
|
+
var SkillSyncTargetEntrySchema = z2.object({
|
|
2511
|
+
target: TargetSchema,
|
|
2512
|
+
last_pulled_at: IsoDateTimeSchema
|
|
2513
|
+
}).strict();
|
|
2514
|
+
var SkillSyncMachineSchema = z2.object({
|
|
2515
|
+
machine_id: IdSchema,
|
|
2516
|
+
machine_label: z2.string().min(1),
|
|
2517
|
+
targets: z2.array(SkillSyncTargetEntrySchema)
|
|
2518
|
+
}).strict();
|
|
2519
|
+
var SkillSyncStateResponseSchema = z2.object({
|
|
2520
|
+
machines: z2.array(SkillSyncMachineSchema),
|
|
2521
|
+
total_machines: z2.number().int().nonnegative(),
|
|
2522
|
+
total_targets_synced: z2.number().int().nonnegative()
|
|
2523
|
+
}).strict();
|
|
2506
2524
|
|
|
2507
2525
|
// src/lib/output.ts
|
|
2508
2526
|
import chalk from "chalk";
|
|
@@ -2531,6 +2549,21 @@ var log = {
|
|
|
2531
2549
|
},
|
|
2532
2550
|
blank: () => {
|
|
2533
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");
|
|
2534
2567
|
}
|
|
2535
2568
|
};
|
|
2536
2569
|
|
|
@@ -2651,26 +2684,26 @@ import { spawn } from "node:child_process";
|
|
|
2651
2684
|
var CONFIG_DIR2 = join4(homedir3(), ".floom");
|
|
2652
2685
|
var MACHINE_FILE = join4(CONFIG_DIR2, "machine.json");
|
|
2653
2686
|
async function tryCommand(cmd, args) {
|
|
2654
|
-
return new Promise((
|
|
2687
|
+
return new Promise((resolve4) => {
|
|
2655
2688
|
let child;
|
|
2656
2689
|
try {
|
|
2657
2690
|
child = spawn(cmd, args, { stdio: ["ignore", "pipe", "ignore"] });
|
|
2658
2691
|
} catch {
|
|
2659
|
-
|
|
2692
|
+
resolve4(null);
|
|
2660
2693
|
return;
|
|
2661
2694
|
}
|
|
2662
2695
|
let out = "";
|
|
2663
2696
|
child.stdout?.on("data", (d) => {
|
|
2664
2697
|
out += d.toString();
|
|
2665
2698
|
});
|
|
2666
|
-
child.on("close", (code) =>
|
|
2667
|
-
child.on("error", () =>
|
|
2699
|
+
child.on("close", (code) => resolve4(code === 0 ? out.trim() : null));
|
|
2700
|
+
child.on("error", () => resolve4(null));
|
|
2668
2701
|
const timer = setTimeout(() => {
|
|
2669
2702
|
try {
|
|
2670
2703
|
child.kill();
|
|
2671
2704
|
} catch {
|
|
2672
2705
|
}
|
|
2673
|
-
|
|
2706
|
+
resolve4(null);
|
|
2674
2707
|
}, 800);
|
|
2675
2708
|
child.on("close", () => clearTimeout(timer));
|
|
2676
2709
|
});
|
|
@@ -2724,7 +2757,7 @@ async function getMachineIdentity() {
|
|
|
2724
2757
|
}
|
|
2725
2758
|
|
|
2726
2759
|
// src/version.ts
|
|
2727
|
-
var VERSION = "2.0.
|
|
2760
|
+
var VERSION = "2.0.6";
|
|
2728
2761
|
|
|
2729
2762
|
// src/api-client.ts
|
|
2730
2763
|
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
@@ -2894,8 +2927,33 @@ async function api(path, opts = {}) {
|
|
|
2894
2927
|
}
|
|
2895
2928
|
|
|
2896
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
|
+
}
|
|
2897
2955
|
function sleep(ms) {
|
|
2898
|
-
return new Promise((
|
|
2956
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
2899
2957
|
}
|
|
2900
2958
|
async function loginCommand() {
|
|
2901
2959
|
const session = await api("/cli/device/start", {
|
|
@@ -2905,10 +2963,11 @@ async function loginCommand() {
|
|
|
2905
2963
|
client: `floom-cli/${VERSION}`
|
|
2906
2964
|
}
|
|
2907
2965
|
});
|
|
2908
|
-
log.heading("
|
|
2909
|
-
log.info(`Open:
|
|
2910
|
-
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)}`);
|
|
2911
2969
|
log.info("Waiting for browser approval. Press Ctrl+C to cancel.");
|
|
2970
|
+
tryOpenBrowser(session.verification_uri);
|
|
2912
2971
|
const deadline = new Date(session.expires_at).getTime();
|
|
2913
2972
|
const interval = Math.max(2, session.poll_interval_seconds) * 1e3;
|
|
2914
2973
|
while (Date.now() < deadline) {
|
|
@@ -2926,7 +2985,9 @@ async function loginCommand() {
|
|
|
2926
2985
|
email: token.user?.email ?? "unknown@example.com",
|
|
2927
2986
|
apiUrl: getApiUrl()
|
|
2928
2987
|
});
|
|
2929
|
-
|
|
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");
|
|
2930
2991
|
return;
|
|
2931
2992
|
}
|
|
2932
2993
|
} catch (error) {
|
|
@@ -2954,9 +3015,10 @@ import { z as z3 } from "zod";
|
|
|
2954
3015
|
// src/commands/sync.ts
|
|
2955
3016
|
import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
|
|
2956
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";
|
|
2957
|
-
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";
|
|
2958
3019
|
import { createInterface } from "node:readline/promises";
|
|
2959
3020
|
import { ZodError } from "zod";
|
|
3021
|
+
import chalk3 from "chalk";
|
|
2960
3022
|
|
|
2961
3023
|
// src/lib/signals.ts
|
|
2962
3024
|
import { rmSync } from "node:fs";
|
|
@@ -3080,6 +3142,83 @@ async function fileExists(path) {
|
|
|
3080
3142
|
return false;
|
|
3081
3143
|
}
|
|
3082
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
|
+
}
|
|
3083
3222
|
async function liveSkillHash(root, workspaceSlug, skill) {
|
|
3084
3223
|
const slug = safeSkillSlug(skill.slug);
|
|
3085
3224
|
const fileHashes = [];
|
|
@@ -3237,41 +3376,119 @@ function printNoDetectedTargets() {
|
|
|
3237
3376
|
log.info(` Or pick one explicitly: npx -y @floomhq/floom pull --target claude`);
|
|
3238
3377
|
log.info(` More help: https://floom.dev/docs#agents`);
|
|
3239
3378
|
}
|
|
3240
|
-
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 = {}) {
|
|
3241
3458
|
if (!options.target) {
|
|
3242
|
-
const
|
|
3243
|
-
if (
|
|
3459
|
+
const targetDirs = await resolveAutoStatusTargets();
|
|
3460
|
+
if (targetDirs.length === 0) {
|
|
3244
3461
|
printNoDetectedTargets();
|
|
3245
3462
|
process.exitCode = 1;
|
|
3246
3463
|
return;
|
|
3247
3464
|
}
|
|
3248
|
-
printDetectedTargets(
|
|
3249
|
-
|
|
3250
|
-
for (const target2 of
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
for (const result of results) {
|
|
3258
|
-
if (!result.ok) {
|
|
3259
|
-
log.err(`${result.target} ${result.error.message}`);
|
|
3260
|
-
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
|
+
}
|
|
3261
3474
|
}
|
|
3262
|
-
log.heading(`${result.status.workspaceName} (${result.target})`);
|
|
3263
|
-
for (const line of result.status.skills) log.info(`${line.slug} ${line.state} ${line.version}`);
|
|
3264
3475
|
}
|
|
3265
|
-
if (
|
|
3476
|
+
if (anyFailed2) process.exitCode = 1;
|
|
3266
3477
|
return;
|
|
3267
3478
|
}
|
|
3268
3479
|
const target = assertInstallTarget(options.target);
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
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;
|
|
3272
3489
|
}
|
|
3273
3490
|
async function statusLibrary(target, deps = {}) {
|
|
3274
|
-
const resolved = resolveInstallDir({ target, global: true });
|
|
3491
|
+
const resolved = { dir: deps.installDir ?? resolveInstallDir({ target, global: true }).dir };
|
|
3275
3492
|
const remote = await (deps.fetchLibrary ?? fetchLibrary)(target, "status");
|
|
3276
3493
|
const manifest = await readManifest(resolved.dir);
|
|
3277
3494
|
if (!manifest) {
|
|
@@ -3309,7 +3526,7 @@ async function statusLibrary(target, deps = {}) {
|
|
|
3309
3526
|
}
|
|
3310
3527
|
return { workspaceName: remote.workspace.name, skills };
|
|
3311
3528
|
}
|
|
3312
|
-
async function pullCommand(options) {
|
|
3529
|
+
async function pullCommand(options, deps = {}) {
|
|
3313
3530
|
const cleanup = installCancellationHandler();
|
|
3314
3531
|
try {
|
|
3315
3532
|
if (!options.target) {
|
|
@@ -3324,39 +3541,50 @@ async function pullCommand(options) {
|
|
|
3324
3541
|
log.info("Cancelled.");
|
|
3325
3542
|
return;
|
|
3326
3543
|
}
|
|
3544
|
+
const dirsByTarget = /* @__PURE__ */ new Map();
|
|
3327
3545
|
for (const target2 of targets) {
|
|
3328
|
-
const
|
|
3329
|
-
|
|
3546
|
+
const dirs2 = await resolvePullDirs(target2);
|
|
3547
|
+
dirsByTarget.set(target2, dirs2);
|
|
3548
|
+
for (const dir of dirs2) cleanup.trackDir(join5(dir, ".floom", "tmp"));
|
|
3330
3549
|
}
|
|
3331
3550
|
const results = [];
|
|
3332
3551
|
for (const target2 of targets) {
|
|
3333
3552
|
try {
|
|
3334
|
-
const
|
|
3335
|
-
|
|
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 });
|
|
3336
3560
|
} catch (error) {
|
|
3337
3561
|
results.push({ target: target2, ok: false, error });
|
|
3338
3562
|
}
|
|
3339
3563
|
}
|
|
3340
3564
|
log.heading("Pull summary:");
|
|
3341
|
-
for (const
|
|
3342
|
-
if (
|
|
3343
|
-
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}`);
|
|
3344
3568
|
}
|
|
3345
|
-
if (results.some((
|
|
3569
|
+
if (results.some((result) => !result.ok)) process.exitCode = 1;
|
|
3346
3570
|
return;
|
|
3347
3571
|
}
|
|
3348
3572
|
const target = assertInstallTarget(options.target);
|
|
3349
|
-
const
|
|
3350
|
-
cleanup.trackDir(join5(
|
|
3351
|
-
|
|
3352
|
-
|
|
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(", ")}).`);
|
|
3353
3581
|
log.info(`This syncs ${target} only. For another agent: npx -y @floomhq/floom pull --target <claude|codex|cursor|gemini|opencode>`);
|
|
3354
3582
|
} finally {
|
|
3355
3583
|
cleanup.dispose();
|
|
3356
3584
|
}
|
|
3357
3585
|
}
|
|
3358
3586
|
async function pullLibrary(target, deps = {}) {
|
|
3359
|
-
const resolved = resolveInstallDir({ target, global: true });
|
|
3587
|
+
const resolved = { dir: deps.installDir ?? resolveInstallDir({ target, global: true }).dir };
|
|
3360
3588
|
const remote = await (deps.fetchLibrary ?? fetchLibrary)(target, "pull");
|
|
3361
3589
|
for (const skill of remote.skills) safeSkillSlug(skill.slug);
|
|
3362
3590
|
const manifest = await readManifest(resolved.dir);
|
|
@@ -3616,13 +3844,19 @@ function createMcpServer(deps = {}) {
|
|
|
3616
3844
|
return server;
|
|
3617
3845
|
}
|
|
3618
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
|
+
}
|
|
3619
3853
|
const server = createMcpServer();
|
|
3620
3854
|
const transport = new StdioServerTransport();
|
|
3621
3855
|
await server.connect(transport);
|
|
3622
3856
|
}
|
|
3623
3857
|
|
|
3624
3858
|
// src/commands/push.ts
|
|
3625
|
-
import { basename, join as join6, resolve as
|
|
3859
|
+
import { basename, join as join6, resolve as resolve3 } from "node:path";
|
|
3626
3860
|
import { readdir as readdir3, readFile as readFile6, stat as stat4 } from "node:fs/promises";
|
|
3627
3861
|
function parseConcurrency(value) {
|
|
3628
3862
|
const raw = value ?? 6;
|
|
@@ -3687,7 +3921,7 @@ async function runBounded(items, concurrency, worker) {
|
|
|
3687
3921
|
async function pushCommand(dir = ".", options = {}, deps = {}) {
|
|
3688
3922
|
const cleanup = installCancellationHandler();
|
|
3689
3923
|
try {
|
|
3690
|
-
const root =
|
|
3924
|
+
const root = resolve3(dir);
|
|
3691
3925
|
const dirStat = await stat4(root).catch(() => null);
|
|
3692
3926
|
if (!dirStat || !dirStat.isDirectory()) {
|
|
3693
3927
|
throw new Error(
|
|
@@ -3699,6 +3933,7 @@ async function pushCommand(dir = ".", options = {}, deps = {}) {
|
|
|
3699
3933
|
if (await hasSkillMarkdown(root)) {
|
|
3700
3934
|
const result = await pushOneSkill(root, pushApi);
|
|
3701
3935
|
log.ok(`Pushed ${result.skill.slug} ${result.skill.latest.display}.`);
|
|
3936
|
+
log.next("Next: pull this skill into your AI agents.", "floom pull");
|
|
3702
3937
|
return;
|
|
3703
3938
|
}
|
|
3704
3939
|
const skillDirs = await findImmediateSkillDirs(root);
|
|
@@ -3715,7 +3950,7 @@ async function pushCommand(dir = ".", options = {}, deps = {}) {
|
|
|
3715
3950
|
try {
|
|
3716
3951
|
const result = await pushOneSkill(skillDir, pushApi);
|
|
3717
3952
|
pushed += 1;
|
|
3718
|
-
log.
|
|
3953
|
+
log.ok(`${result.skill.slug} ${result.skill.latest.display}`);
|
|
3719
3954
|
} catch (error) {
|
|
3720
3955
|
errors.push({ slug, message: error.message });
|
|
3721
3956
|
}
|
|
@@ -3752,19 +3987,196 @@ async function deleteCommand(slug, opts = {}, deps = defaultDeps) {
|
|
|
3752
3987
|
async function listCommand() {
|
|
3753
3988
|
const result = await api("/skills", { authRequired: true });
|
|
3754
3989
|
if (result.total === 0) {
|
|
3755
|
-
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");
|
|
3756
3992
|
return;
|
|
3757
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);
|
|
3758
3996
|
for (const skill of result.skills) {
|
|
3759
|
-
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.`);
|
|
3760
4172
|
}
|
|
3761
4173
|
}
|
|
3762
4174
|
|
|
3763
4175
|
// src/commands/rename-machine.ts
|
|
3764
4176
|
import { readFile as readFile7, writeFile as writeFile4 } from "node:fs/promises";
|
|
3765
|
-
import { join as
|
|
4177
|
+
import { join as join8 } from "node:path";
|
|
3766
4178
|
import { homedir as homedir4 } from "node:os";
|
|
3767
|
-
var MACHINE_FILE2 =
|
|
4179
|
+
var MACHINE_FILE2 = join8(homedir4(), ".floom", "machine.json");
|
|
3768
4180
|
async function renameMachineCommand(newLabel, _opts) {
|
|
3769
4181
|
const trimmed = newLabel.trim().slice(0, 80);
|
|
3770
4182
|
if (!trimmed) {
|
|
@@ -3803,8 +4215,8 @@ async function renameMachineCommand(newLabel, _opts) {
|
|
|
3803
4215
|
}
|
|
3804
4216
|
|
|
3805
4217
|
// src/commands/add.ts
|
|
3806
|
-
import { mkdir as mkdir5, rm as
|
|
3807
|
-
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";
|
|
3808
4220
|
var TOKEN_RE = /^fls_[A-Za-z0-9_-]{32,}$/;
|
|
3809
4221
|
function parseToken(input) {
|
|
3810
4222
|
const trimmed = input.trim();
|
|
@@ -3899,13 +4311,15 @@ async function fetchPublicShare(token) {
|
|
|
3899
4311
|
} catch {
|
|
3900
4312
|
throw new Error("Unexpected response from Floom API. Try again in a moment.");
|
|
3901
4313
|
}
|
|
3902
|
-
if (json
|
|
3903
|
-
|
|
4314
|
+
if (json && typeof json === "object" && "error" in json) {
|
|
4315
|
+
const errObj = json.error;
|
|
4316
|
+
if (errObj) throw new Error(errObj.message ?? "API error");
|
|
3904
4317
|
}
|
|
3905
|
-
|
|
4318
|
+
const candidate = json;
|
|
4319
|
+
if (!candidate || typeof candidate !== "object" || !candidate.skill || !Array.isArray(candidate.file_contents)) {
|
|
3906
4320
|
throw new Error("Unexpected response shape from Floom API.");
|
|
3907
4321
|
}
|
|
3908
|
-
return
|
|
4322
|
+
return candidate;
|
|
3909
4323
|
}
|
|
3910
4324
|
async function addCommand(input, opts = {}) {
|
|
3911
4325
|
const token = parseToken(input);
|
|
@@ -3955,27 +4369,27 @@ async function addCommand(input, opts = {}) {
|
|
|
3955
4369
|
safeRemotePath2(file.path);
|
|
3956
4370
|
}
|
|
3957
4371
|
const cleanup = installCancellationHandler();
|
|
3958
|
-
const tempDir =
|
|
4372
|
+
const tempDir = join9(resolved.dir, ".floom", "tmp", `${slug}-add-${Date.now()}`);
|
|
3959
4373
|
cleanup.trackDir(tempDir);
|
|
3960
4374
|
try {
|
|
3961
4375
|
await mkdir5(tempDir, { recursive: true });
|
|
3962
4376
|
for (const file of shareData.file_contents) {
|
|
3963
4377
|
const safePath = safeRemotePath2(file.path);
|
|
3964
|
-
const dest =
|
|
4378
|
+
const dest = join9(tempDir, ...safePath.split("/"));
|
|
3965
4379
|
const destDir = dest.substring(0, dest.lastIndexOf("/"));
|
|
3966
4380
|
if (destDir !== tempDir) await mkdir5(destDir, { recursive: true });
|
|
3967
4381
|
await writeFile5(dest, bytesForShareFile(file));
|
|
3968
4382
|
}
|
|
3969
|
-
const finalDir =
|
|
3970
|
-
const replacedDir =
|
|
4383
|
+
const finalDir = join9(resolved.dir, slug);
|
|
4384
|
+
const replacedDir = join9(resolved.dir, ".floom", "tmp", `${slug}-previous-${Date.now()}`);
|
|
3971
4385
|
let movedExisting = false;
|
|
3972
4386
|
try {
|
|
3973
4387
|
const { rename: rename2, rm: rmFs } = await import("node:fs/promises");
|
|
3974
4388
|
await mkdir5(resolved.dir, { recursive: true });
|
|
3975
|
-
const { stat:
|
|
4389
|
+
const { stat: stat6 } = await import("node:fs/promises");
|
|
3976
4390
|
let existingDir = false;
|
|
3977
4391
|
try {
|
|
3978
|
-
await
|
|
4392
|
+
await stat6(finalDir);
|
|
3979
4393
|
existingDir = true;
|
|
3980
4394
|
} catch {
|
|
3981
4395
|
}
|
|
@@ -4009,7 +4423,7 @@ async function addCommand(input, opts = {}) {
|
|
|
4009
4423
|
} finally {
|
|
4010
4424
|
cleanup.dispose();
|
|
4011
4425
|
try {
|
|
4012
|
-
await
|
|
4426
|
+
await rm3(tempDir, { recursive: true, force: true });
|
|
4013
4427
|
} catch {
|
|
4014
4428
|
}
|
|
4015
4429
|
}
|
|
@@ -4059,18 +4473,30 @@ async function whoamiCommand() {
|
|
|
4059
4473
|
}
|
|
4060
4474
|
}
|
|
4061
4475
|
var program = new Command();
|
|
4062
|
-
program.name("floom").description("Floom
|
|
4063
|
-
|
|
4064
|
-
program.
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
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);
|
|
4069
4495
|
program.command("list").description("List workspace skills.").action(listCommand);
|
|
4070
4496
|
program.command("status").description("Show local workspace sync status.").option("--target <target>", "claude | codex | cursor | gemini | opencode").action(statusCommand);
|
|
4071
4497
|
program.command("mcp").description("Run the local MCP server over stdio.").action(mcpCommand);
|
|
4072
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));
|
|
4073
|
-
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);
|
|
4074
4500
|
async function main() {
|
|
4075
4501
|
try {
|
|
4076
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",
|