@floomhq/floom 2.0.2 → 2.0.4
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 +335 -24
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2477,6 +2477,32 @@ var ContractDecisionChecklistItemSchema = z2.object({
|
|
|
2477
2477
|
title: z2.string().min(1),
|
|
2478
2478
|
locked: z2.literal(true)
|
|
2479
2479
|
}).strict();
|
|
2480
|
+
var AGENT_TARGETS = ["claude", "codex", "cursor", "gemini", "opencode"];
|
|
2481
|
+
var AgentTargetSyncSchema = z2.object({
|
|
2482
|
+
last_pulled_at: IsoDateTimeSchema.nullable()
|
|
2483
|
+
}).strict();
|
|
2484
|
+
var AgentMachineSchema = z2.object({
|
|
2485
|
+
machine_id: z2.string().uuid(),
|
|
2486
|
+
machine_label: z2.string().min(1),
|
|
2487
|
+
last_seen_at: IsoDateTimeSchema,
|
|
2488
|
+
target_sync: z2.record(z2.enum(AGENT_TARGETS), AgentTargetSyncSchema)
|
|
2489
|
+
}).strict();
|
|
2490
|
+
var AgentsListResponseSchema = z2.object({
|
|
2491
|
+
machines: z2.array(AgentMachineSchema),
|
|
2492
|
+
total: z2.number().int().nonnegative()
|
|
2493
|
+
}).strict();
|
|
2494
|
+
var ActivityEventSchema = z2.object({
|
|
2495
|
+
id: IdSchema,
|
|
2496
|
+
event_name: z2.enum(["signup", "workspace_create", "push", "pull", "share_create", "skill_delete"]),
|
|
2497
|
+
source: z2.enum(["web", "cli", "api"]),
|
|
2498
|
+
target: z2.enum(AGENT_TARGETS).nullable(),
|
|
2499
|
+
skill_slug: z2.string().nullable(),
|
|
2500
|
+
created_at: IsoDateTimeSchema
|
|
2501
|
+
}).strict();
|
|
2502
|
+
var ActivityFeedResponseSchema = z2.object({
|
|
2503
|
+
events: z2.array(ActivityEventSchema),
|
|
2504
|
+
total: z2.number().int().nonnegative()
|
|
2505
|
+
}).strict();
|
|
2480
2506
|
|
|
2481
2507
|
// src/lib/output.ts
|
|
2482
2508
|
import chalk from "chalk";
|
|
@@ -2563,14 +2589,20 @@ async function readRawAuth() {
|
|
|
2563
2589
|
const code = e.code;
|
|
2564
2590
|
if (code === "ENOENT") return null;
|
|
2565
2591
|
if (code === "EACCES" || code === "EPERM") {
|
|
2566
|
-
throw new Error(
|
|
2592
|
+
throw new Error(
|
|
2593
|
+
"Cannot read your Floom auth file (permission denied).\n Fix: chmod 600 ~/.floom/auth.json\n Or re-authenticate: npx -y @floomhq/floom login\n More help: https://floom.dev/docs#troubleshooting"
|
|
2594
|
+
);
|
|
2567
2595
|
}
|
|
2568
|
-
throw new Error(
|
|
2596
|
+
throw new Error(
|
|
2597
|
+
"Cannot read your Floom auth file.\n Run: npx -y @floomhq/floom login\n More help: https://floom.dev/docs#troubleshooting"
|
|
2598
|
+
);
|
|
2569
2599
|
}
|
|
2570
2600
|
try {
|
|
2571
2601
|
return JSON.parse(raw);
|
|
2572
2602
|
} catch {
|
|
2573
|
-
throw new Error(
|
|
2603
|
+
throw new Error(
|
|
2604
|
+
"Your Floom auth file is corrupted.\n Run: npx -y @floomhq/floom login to generate a fresh one.\n More help: https://floom.dev/docs#troubleshooting"
|
|
2605
|
+
);
|
|
2574
2606
|
}
|
|
2575
2607
|
}
|
|
2576
2608
|
async function writeAuth(state) {
|
|
@@ -2692,7 +2724,7 @@ async function getMachineIdentity() {
|
|
|
2692
2724
|
}
|
|
2693
2725
|
|
|
2694
2726
|
// src/version.ts
|
|
2695
|
-
var VERSION = "2.0.
|
|
2727
|
+
var VERSION = "2.0.4";
|
|
2696
2728
|
|
|
2697
2729
|
// src/api-client.ts
|
|
2698
2730
|
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
@@ -2739,11 +2771,54 @@ async function readLimitedText(res, limitBytes = MAX_ERROR_BODY_BYTES) {
|
|
|
2739
2771
|
return truncated ? `${text}
|
|
2740
2772
|
[truncated]` : text;
|
|
2741
2773
|
}
|
|
2774
|
+
var HELP = "https://floom.dev/docs#troubleshooting";
|
|
2775
|
+
function friendlyApiErrorMessage(code, raw, status) {
|
|
2776
|
+
if (code === "AUTH_REQUIRED" || code === "TOKEN_INVALID" || status === 401) {
|
|
2777
|
+
return `Not signed in.
|
|
2778
|
+
Run: npx -y @floomhq/floom login
|
|
2779
|
+
More help: ${HELP}`;
|
|
2780
|
+
}
|
|
2781
|
+
if (code === "TOKEN_EXPIRED") {
|
|
2782
|
+
return `Your session has expired.
|
|
2783
|
+
Run: npx -y @floomhq/floom login
|
|
2784
|
+
More help: ${HELP}`;
|
|
2785
|
+
}
|
|
2786
|
+
if (code === "SKILL_ACCESS_DENIED" || status === 403) {
|
|
2787
|
+
return `Access denied. This skill belongs to another workspace or you don't have permission to modify it.
|
|
2788
|
+
If you want your own copy, push it with a different slug. More help: ${HELP}`;
|
|
2789
|
+
}
|
|
2790
|
+
if (code === "SKILL_NOT_FOUND" || status === 404) {
|
|
2791
|
+
return `Skill not found. It may have been deleted or the slug is wrong.
|
|
2792
|
+
Run: npx -y @floomhq/floom list
|
|
2793
|
+
More help: ${HELP}`;
|
|
2794
|
+
}
|
|
2795
|
+
if (code === "RATE_LIMITED" || status === 429) {
|
|
2796
|
+
return `Too many requests. Wait a moment and try again.
|
|
2797
|
+
More help: ${HELP}`;
|
|
2798
|
+
}
|
|
2799
|
+
if (code === "CLI_VERSION_TOO_OLD") {
|
|
2800
|
+
return `Your CLI is out of date.
|
|
2801
|
+
Run: npx -y @floomhq/floom@latest <command>
|
|
2802
|
+
More help: ${HELP}`;
|
|
2803
|
+
}
|
|
2804
|
+
if (code === "FILE_TOO_LARGE" || code === "BUNDLE_TOO_LARGE") {
|
|
2805
|
+
return `${raw}
|
|
2806
|
+
Reduce the size of your skill files before pushing. More help: ${HELP}`;
|
|
2807
|
+
}
|
|
2808
|
+
if (status && status >= 500) {
|
|
2809
|
+
return `Floom service error (${status}). Please try again in a moment.
|
|
2810
|
+
More help: ${HELP}`;
|
|
2811
|
+
}
|
|
2812
|
+
return raw;
|
|
2813
|
+
}
|
|
2742
2814
|
async function api(path, opts = {}) {
|
|
2743
2815
|
const auth = await readAuth();
|
|
2744
2816
|
const token = process.env.FLOOM_API_TOKEN?.trim() || auth?.token;
|
|
2745
2817
|
if (opts.authRequired && !token) {
|
|
2746
|
-
throw new FloomError(
|
|
2818
|
+
throw new FloomError(
|
|
2819
|
+
"AUTH_REQUIRED",
|
|
2820
|
+
"Not signed in.\n Run: npx -y @floomhq/floom login\n More help: https://floom.dev/docs#troubleshooting"
|
|
2821
|
+
);
|
|
2747
2822
|
}
|
|
2748
2823
|
let lastError = null;
|
|
2749
2824
|
const bases = getApiBaseUrls(auth?.apiUrl);
|
|
@@ -2776,7 +2851,9 @@ async function api(path, opts = {}) {
|
|
|
2776
2851
|
} catch (e) {
|
|
2777
2852
|
lastError = new FloomError(
|
|
2778
2853
|
"INTERNAL_ERROR",
|
|
2779
|
-
`
|
|
2854
|
+
`Cannot reach Floom (${base}): ${e.message}
|
|
2855
|
+
Check your internet connection or firewall. If on a corporate network, you may need to set a proxy: npm config set proxy http://...
|
|
2856
|
+
More help: https://floom.dev/docs#troubleshooting`,
|
|
2780
2857
|
{ apiUrl: base }
|
|
2781
2858
|
);
|
|
2782
2859
|
continue;
|
|
@@ -2796,17 +2873,19 @@ async function api(path, opts = {}) {
|
|
|
2796
2873
|
}
|
|
2797
2874
|
const envelopeError = json?.error;
|
|
2798
2875
|
if (envelopeError && typeof envelopeError === "object" && envelopeError.code) {
|
|
2876
|
+
const rawMsg2 = typeof envelopeError.message === "string" ? envelopeError.message : String(envelopeError.code);
|
|
2799
2877
|
throw new FloomError(
|
|
2800
2878
|
envelopeError.code,
|
|
2801
|
-
|
|
2879
|
+
friendlyApiErrorMessage(envelopeError.code, rawMsg2, res.status),
|
|
2802
2880
|
{ status: res.status, requestId: envelopeError.request_id, apiUrl: base }
|
|
2803
2881
|
);
|
|
2804
2882
|
}
|
|
2805
2883
|
if (res.ok) return json;
|
|
2806
2884
|
const err = envelopeError ?? {};
|
|
2885
|
+
const rawMsg = err.message ?? `HTTP ${res.status} ${res.statusText}`;
|
|
2807
2886
|
lastError = new FloomError(
|
|
2808
2887
|
err.code ?? "INTERNAL_ERROR",
|
|
2809
|
-
err.
|
|
2888
|
+
friendlyApiErrorMessage(err.code ?? "INTERNAL_ERROR", rawMsg, res.status),
|
|
2810
2889
|
{ status: res.status, requestId: err.request_id, apiUrl: base }
|
|
2811
2890
|
);
|
|
2812
2891
|
if (res.status !== 404 || json?.error) break;
|
|
@@ -2862,7 +2941,9 @@ async function loginCommand() {
|
|
|
2862
2941
|
if (remaining <= 0) break;
|
|
2863
2942
|
await sleep(Math.min(interval, remaining));
|
|
2864
2943
|
}
|
|
2865
|
-
throw new Error(
|
|
2944
|
+
throw new Error(
|
|
2945
|
+
"Login timed out.\n The browser approval window expired. Run: npx -y @floomhq/floom login\n More help: https://floom.dev/docs#troubleshooting"
|
|
2946
|
+
);
|
|
2866
2947
|
}
|
|
2867
2948
|
|
|
2868
2949
|
// src/commands/mcp.ts
|
|
@@ -2936,7 +3017,9 @@ async function readManifest(root) {
|
|
|
2936
3017
|
if (err.code === "ENOENT") return null;
|
|
2937
3018
|
if (error instanceof SyntaxError || error instanceof ZodError) {
|
|
2938
3019
|
throw new Error(
|
|
2939
|
-
`Corrupted manifest at ${manifestPath(root)}. Remove that file or run
|
|
3020
|
+
`Corrupted manifest at ${manifestPath(root)}. Remove that file or run floom pull after backing up local edits.
|
|
3021
|
+
Run: rm ${manifestPath(root)} && npx -y @floomhq/floom pull
|
|
3022
|
+
More help: https://floom.dev/docs#troubleshooting`
|
|
2940
3023
|
);
|
|
2941
3024
|
}
|
|
2942
3025
|
throw error;
|
|
@@ -3148,8 +3231,11 @@ function printDetectedTargets(targets) {
|
|
|
3148
3231
|
for (const target of targets) log.info(`- ${target}`);
|
|
3149
3232
|
}
|
|
3150
3233
|
function printNoDetectedTargets() {
|
|
3151
|
-
log.
|
|
3152
|
-
log.info(`
|
|
3234
|
+
log.err(`No AI agent recognized on this machine.`);
|
|
3235
|
+
log.info(` Floom looks for: ${INSTALL_TARGETS.join(", ")}.`);
|
|
3236
|
+
log.info(` Make sure one of these is installed, then re-run.`);
|
|
3237
|
+
log.info(` Or pick one explicitly: npx -y @floomhq/floom pull --target claude`);
|
|
3238
|
+
log.info(` More help: https://floom.dev/docs#agents`);
|
|
3153
3239
|
}
|
|
3154
3240
|
async function statusCommand(options) {
|
|
3155
3241
|
if (!options.target) {
|
|
@@ -3563,11 +3649,17 @@ async function findImmediateSkillDirs(root) {
|
|
|
3563
3649
|
async function pushOneSkill(root, pushApi) {
|
|
3564
3650
|
const dirStat = await stat4(root).catch(() => null);
|
|
3565
3651
|
if (!dirStat || !dirStat.isDirectory()) {
|
|
3566
|
-
throw new Error(
|
|
3652
|
+
throw new Error(
|
|
3653
|
+
`Folder not found: ${root}
|
|
3654
|
+
Check the path and try again. Run from your skill folder or pass the path: npx -y @floomhq/floom push ./path/to/skill`
|
|
3655
|
+
);
|
|
3567
3656
|
}
|
|
3568
3657
|
const bundle = await collectBundle(root);
|
|
3569
3658
|
const skillMd = bundle.files.find((file) => file.relPath === "SKILL.md");
|
|
3570
|
-
if (!skillMd) throw new Error(
|
|
3659
|
+
if (!skillMd) throw new Error(
|
|
3660
|
+
`Missing SKILL.md in ${root}.
|
|
3661
|
+
Every skill folder needs a SKILL.md at its root. See: https://floom.dev/docs#concept`
|
|
3662
|
+
);
|
|
3571
3663
|
const files = await Promise.all(bundle.files.map(async (file) => ({
|
|
3572
3664
|
path: file.relPath,
|
|
3573
3665
|
content_base64: (await readFile6(file.absPath)).toString("base64")
|
|
@@ -3598,7 +3690,10 @@ async function pushCommand(dir = ".", options = {}, deps = {}) {
|
|
|
3598
3690
|
const root = resolve2(dir);
|
|
3599
3691
|
const dirStat = await stat4(root).catch(() => null);
|
|
3600
3692
|
if (!dirStat || !dirStat.isDirectory()) {
|
|
3601
|
-
throw new Error(
|
|
3693
|
+
throw new Error(
|
|
3694
|
+
`Folder not found: ${dir}
|
|
3695
|
+
Check the path and try again. Run from your skill folder or pass the path: npx -y @floomhq/floom push ./path/to/skill`
|
|
3696
|
+
);
|
|
3602
3697
|
}
|
|
3603
3698
|
const pushApi = deps.pushApi ?? api;
|
|
3604
3699
|
if (await hasSkillMarkdown(root)) {
|
|
@@ -3607,7 +3702,10 @@ async function pushCommand(dir = ".", options = {}, deps = {}) {
|
|
|
3607
3702
|
return;
|
|
3608
3703
|
}
|
|
3609
3704
|
const skillDirs = await findImmediateSkillDirs(root);
|
|
3610
|
-
if (skillDirs.length === 0) throw new Error(
|
|
3705
|
+
if (skillDirs.length === 0) throw new Error(
|
|
3706
|
+
`No skills found in ${dir}.
|
|
3707
|
+
Neither the folder itself nor any immediate subfolder contains a SKILL.md. See: https://floom.dev/docs#concept`
|
|
3708
|
+
);
|
|
3611
3709
|
const concurrency = parseConcurrency(options.concurrency);
|
|
3612
3710
|
const startedAt = Date.now();
|
|
3613
3711
|
const errors = [];
|
|
@@ -3704,6 +3802,219 @@ async function renameMachineCommand(newLabel, _opts) {
|
|
|
3704
3802
|
}
|
|
3705
3803
|
}
|
|
3706
3804
|
|
|
3805
|
+
// src/commands/add.ts
|
|
3806
|
+
import { mkdir as mkdir5, rm as rm2, writeFile as writeFile5 } from "node:fs/promises";
|
|
3807
|
+
import { join as join8 } from "node:path";
|
|
3808
|
+
var TOKEN_RE = /^fls_[A-Za-z0-9_-]{32,}$/;
|
|
3809
|
+
function parseToken(input) {
|
|
3810
|
+
const trimmed = input.trim();
|
|
3811
|
+
if (TOKEN_RE.test(trimmed)) return trimmed;
|
|
3812
|
+
try {
|
|
3813
|
+
const url = new URL(trimmed);
|
|
3814
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
3815
|
+
if (parts.length >= 2 && parts[parts.length - 2] === "s") {
|
|
3816
|
+
const candidate = parts[parts.length - 1];
|
|
3817
|
+
if (candidate && TOKEN_RE.test(candidate)) return candidate;
|
|
3818
|
+
}
|
|
3819
|
+
const last = parts[parts.length - 1];
|
|
3820
|
+
if (last && TOKEN_RE.test(last)) return last;
|
|
3821
|
+
} catch {
|
|
3822
|
+
}
|
|
3823
|
+
return null;
|
|
3824
|
+
}
|
|
3825
|
+
var SAFE_SLUG_RE2 = /^(?=.{1,80}$)[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
3826
|
+
var MAX_DECODED_FILE_BYTES2 = 2 * 1024 * 1024;
|
|
3827
|
+
function safeSkillSlug2(slug) {
|
|
3828
|
+
if (!slug) throw new Error("Remote skill slug is empty.");
|
|
3829
|
+
if (slug.length > 80) throw new Error(`Remote skill slug "${slug.slice(0, 80)}\u2026" exceeds 80 characters.`);
|
|
3830
|
+
if (!SAFE_SLUG_RE2.test(slug)) throw new Error(`Unsafe remote skill slug: ${slug}`);
|
|
3831
|
+
return slug;
|
|
3832
|
+
}
|
|
3833
|
+
function safeRemotePath2(path) {
|
|
3834
|
+
const normalized = path.replace(/\\/g, "/");
|
|
3835
|
+
if (!normalized || /[\x00-\x1f\x7f]/.test(normalized) || normalized.startsWith("/") || normalized.split("/").some((part) => part === "" || part === "." || part === "..")) {
|
|
3836
|
+
throw new Error(`Unsafe remote file path: ${path}`);
|
|
3837
|
+
}
|
|
3838
|
+
return normalized;
|
|
3839
|
+
}
|
|
3840
|
+
function bytesForShareFile(file) {
|
|
3841
|
+
if (file.mode === "text") {
|
|
3842
|
+
const text = file.content_text ?? "";
|
|
3843
|
+
if (Buffer.byteLength(text, "utf8") > MAX_DECODED_FILE_BYTES2) {
|
|
3844
|
+
throw new Error(`Remote file ${file.path} exceeds the 2 MiB decoded file limit.`);
|
|
3845
|
+
}
|
|
3846
|
+
return Buffer.from(text, "utf8");
|
|
3847
|
+
}
|
|
3848
|
+
const url = file.binary?.download_url ?? null;
|
|
3849
|
+
if (!url?.startsWith("data:")) return Buffer.alloc(0);
|
|
3850
|
+
const [, encoded] = url.split(",", 2);
|
|
3851
|
+
const base64 = encoded ?? "";
|
|
3852
|
+
const bytes = Buffer.from(base64, "base64");
|
|
3853
|
+
if (bytes.byteLength > MAX_DECODED_FILE_BYTES2) {
|
|
3854
|
+
throw new Error(`Remote file ${file.path} exceeds the 2 MiB decoded file limit.`);
|
|
3855
|
+
}
|
|
3856
|
+
return bytes;
|
|
3857
|
+
}
|
|
3858
|
+
function derivedSlug(title) {
|
|
3859
|
+
return title.toLowerCase().replace(/[^a-z0-9 -]/g, "").trim().replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, 80).replace(/^-+|-+$/g, "") || "shared-skill";
|
|
3860
|
+
}
|
|
3861
|
+
async function fetchPublicShare(token) {
|
|
3862
|
+
const apiBase = getAppUrl().replace(/\/$/, "");
|
|
3863
|
+
const url = `${apiBase}/api/v1/share/${encodeURIComponent(token)}`;
|
|
3864
|
+
const controller = new AbortController();
|
|
3865
|
+
const timer = setTimeout(() => controller.abort(), 3e4);
|
|
3866
|
+
let res;
|
|
3867
|
+
try {
|
|
3868
|
+
res = await fetch(url, {
|
|
3869
|
+
headers: { "User-Agent": "floom-cli/add" },
|
|
3870
|
+
redirect: "manual",
|
|
3871
|
+
signal: controller.signal
|
|
3872
|
+
});
|
|
3873
|
+
} catch (e) {
|
|
3874
|
+
if (e.name === "AbortError") {
|
|
3875
|
+
throw new Error("Request timed out. Check your internet connection and try again.");
|
|
3876
|
+
}
|
|
3877
|
+
throw new Error(`Cannot reach Floom: ${e.message}`);
|
|
3878
|
+
} finally {
|
|
3879
|
+
clearTimeout(timer);
|
|
3880
|
+
}
|
|
3881
|
+
if (res.status === 404) {
|
|
3882
|
+
throw new FloomError(
|
|
3883
|
+
"SKILL_NOT_FOUND",
|
|
3884
|
+
"Share link not found or revoked. Ask the sender for a fresh link."
|
|
3885
|
+
);
|
|
3886
|
+
}
|
|
3887
|
+
if (res.status === 429) {
|
|
3888
|
+
throw new FloomError(
|
|
3889
|
+
"RATE_LIMITED",
|
|
3890
|
+
"Too many requests. Try again in a minute."
|
|
3891
|
+
);
|
|
3892
|
+
}
|
|
3893
|
+
if (!res.ok) {
|
|
3894
|
+
throw new Error(`Floom API returned HTTP ${res.status}. Try again in a moment.`);
|
|
3895
|
+
}
|
|
3896
|
+
let json;
|
|
3897
|
+
try {
|
|
3898
|
+
json = await res.json();
|
|
3899
|
+
} catch {
|
|
3900
|
+
throw new Error("Unexpected response from Floom API. Try again in a moment.");
|
|
3901
|
+
}
|
|
3902
|
+
if (json.error) {
|
|
3903
|
+
throw new Error(json.error.message ?? "API error");
|
|
3904
|
+
}
|
|
3905
|
+
if (!json.data) {
|
|
3906
|
+
throw new Error("Unexpected response shape from Floom API.");
|
|
3907
|
+
}
|
|
3908
|
+
return json.data;
|
|
3909
|
+
}
|
|
3910
|
+
async function addCommand(input, opts = {}) {
|
|
3911
|
+
const token = parseToken(input);
|
|
3912
|
+
if (!token) {
|
|
3913
|
+
log.err(
|
|
3914
|
+
"That doesn't look like a Floom share link. Should look like: https://floom.dev/s/fls_xxx"
|
|
3915
|
+
);
|
|
3916
|
+
process.exitCode = 1;
|
|
3917
|
+
return;
|
|
3918
|
+
}
|
|
3919
|
+
let target;
|
|
3920
|
+
if (opts.target) {
|
|
3921
|
+
try {
|
|
3922
|
+
target = assertInstallTarget(opts.target);
|
|
3923
|
+
} catch {
|
|
3924
|
+
log.err(`Unknown target "${opts.target}". Use one of: claude, codex, cursor, gemini, opencode`);
|
|
3925
|
+
process.exitCode = 1;
|
|
3926
|
+
return;
|
|
3927
|
+
}
|
|
3928
|
+
} else {
|
|
3929
|
+
const detected = await detectInstalledTargets();
|
|
3930
|
+
if (detected.length === 0) {
|
|
3931
|
+
log.err("No AI agent recognized on this machine.");
|
|
3932
|
+
log.info(" Floom looks for: claude, codex, cursor, gemini, opencode.");
|
|
3933
|
+
log.info(" Or pick one explicitly: npx -y @floomhq/floom add <share-url> --target claude");
|
|
3934
|
+
process.exitCode = 1;
|
|
3935
|
+
return;
|
|
3936
|
+
}
|
|
3937
|
+
target = detected[0];
|
|
3938
|
+
}
|
|
3939
|
+
const resolved = resolveInstallDir({ target, global: true });
|
|
3940
|
+
log.step(`Fetching share\u2026`);
|
|
3941
|
+
let shareData;
|
|
3942
|
+
try {
|
|
3943
|
+
shareData = await fetchPublicShare(token);
|
|
3944
|
+
} catch (e) {
|
|
3945
|
+
if (e instanceof FloomError) {
|
|
3946
|
+
log.err(e.message);
|
|
3947
|
+
} else {
|
|
3948
|
+
log.err(e.message ?? "Unknown error");
|
|
3949
|
+
}
|
|
3950
|
+
process.exitCode = 1;
|
|
3951
|
+
return;
|
|
3952
|
+
}
|
|
3953
|
+
const slug = safeSkillSlug2(derivedSlug(shareData.skill.title));
|
|
3954
|
+
for (const file of shareData.file_contents) {
|
|
3955
|
+
safeRemotePath2(file.path);
|
|
3956
|
+
}
|
|
3957
|
+
const cleanup = installCancellationHandler();
|
|
3958
|
+
const tempDir = join8(resolved.dir, ".floom", "tmp", `${slug}-add-${Date.now()}`);
|
|
3959
|
+
cleanup.trackDir(tempDir);
|
|
3960
|
+
try {
|
|
3961
|
+
await mkdir5(tempDir, { recursive: true });
|
|
3962
|
+
for (const file of shareData.file_contents) {
|
|
3963
|
+
const safePath = safeRemotePath2(file.path);
|
|
3964
|
+
const dest = join8(tempDir, ...safePath.split("/"));
|
|
3965
|
+
const destDir = dest.substring(0, dest.lastIndexOf("/"));
|
|
3966
|
+
if (destDir !== tempDir) await mkdir5(destDir, { recursive: true });
|
|
3967
|
+
await writeFile5(dest, bytesForShareFile(file));
|
|
3968
|
+
}
|
|
3969
|
+
const finalDir = join8(resolved.dir, slug);
|
|
3970
|
+
const replacedDir = join8(resolved.dir, ".floom", "tmp", `${slug}-previous-${Date.now()}`);
|
|
3971
|
+
let movedExisting = false;
|
|
3972
|
+
try {
|
|
3973
|
+
const { rename: rename2, rm: rmFs } = await import("node:fs/promises");
|
|
3974
|
+
await mkdir5(resolved.dir, { recursive: true });
|
|
3975
|
+
const { stat: stat5 } = await import("node:fs/promises");
|
|
3976
|
+
let existingDir = false;
|
|
3977
|
+
try {
|
|
3978
|
+
await stat5(finalDir);
|
|
3979
|
+
existingDir = true;
|
|
3980
|
+
} catch {
|
|
3981
|
+
}
|
|
3982
|
+
if (existingDir) {
|
|
3983
|
+
await rename2(finalDir, replacedDir);
|
|
3984
|
+
movedExisting = true;
|
|
3985
|
+
}
|
|
3986
|
+
try {
|
|
3987
|
+
await rename2(tempDir, finalDir);
|
|
3988
|
+
} catch (renameErr) {
|
|
3989
|
+
if (movedExisting) {
|
|
3990
|
+
try {
|
|
3991
|
+
await rename2(replacedDir, finalDir);
|
|
3992
|
+
} catch {
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3995
|
+
throw new Error(`Failed to install ${slug}: ${renameErr.message}`);
|
|
3996
|
+
}
|
|
3997
|
+
if (movedExisting) await rmFs(replacedDir, { recursive: true, force: true });
|
|
3998
|
+
} catch (e) {
|
|
3999
|
+
throw e;
|
|
4000
|
+
}
|
|
4001
|
+
log.blank();
|
|
4002
|
+
log.ok(`Installed ${shareData.skill.title} ${shareData.skill.version.display} from ${shareData.skill.owner.name}`);
|
|
4003
|
+
log.info(` \u2192 ${finalDir}`);
|
|
4004
|
+
log.blank();
|
|
4005
|
+
log.info(`Next: in your agent, the skill is now loaded under the slug "${slug}".`);
|
|
4006
|
+
} catch (e) {
|
|
4007
|
+
log.err(e.message ?? "Unknown error");
|
|
4008
|
+
process.exitCode = 1;
|
|
4009
|
+
} finally {
|
|
4010
|
+
cleanup.dispose();
|
|
4011
|
+
try {
|
|
4012
|
+
await rm2(tempDir, { recursive: true, force: true });
|
|
4013
|
+
} catch {
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
|
|
3707
4018
|
// src/index.ts
|
|
3708
4019
|
async function logoutCommand() {
|
|
3709
4020
|
const auth = await readAuth();
|
|
@@ -3720,7 +4031,9 @@ async function logoutCommand() {
|
|
|
3720
4031
|
async function whoamiCommand() {
|
|
3721
4032
|
const auth = await readAuth();
|
|
3722
4033
|
if (!auth) {
|
|
3723
|
-
log.
|
|
4034
|
+
log.err("Not signed in.");
|
|
4035
|
+
log.info(" Run: npx -y @floomhq/floom login");
|
|
4036
|
+
log.info(" More help: https://floom.dev/docs#troubleshooting");
|
|
3724
4037
|
process.exitCode = 1;
|
|
3725
4038
|
return;
|
|
3726
4039
|
}
|
|
@@ -3739,12 +4052,9 @@ async function whoamiCommand() {
|
|
|
3739
4052
|
}
|
|
3740
4053
|
log.kv("api url", auth.apiUrl);
|
|
3741
4054
|
} catch (error) {
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
log.err(error.message);
|
|
3746
|
-
}
|
|
3747
|
-
log.warn("Local auth file exists but the server rejected this session. Run: npx -y @floomhq/floom login");
|
|
4055
|
+
log.err(error.message);
|
|
4056
|
+
log.warn("Session invalid. Run: npx -y @floomhq/floom login");
|
|
4057
|
+
log.info(" More help: https://floom.dev/docs#troubleshooting");
|
|
3748
4058
|
process.exitCode = 1;
|
|
3749
4059
|
}
|
|
3750
4060
|
}
|
|
@@ -3759,13 +4069,14 @@ program.command("pull").description("Pull the whole workspace library.").option(
|
|
|
3759
4069
|
program.command("list").description("List workspace skills.").action(listCommand);
|
|
3760
4070
|
program.command("status").description("Show local workspace sync status.").option("--target <target>", "claude | codex | cursor | gemini | opencode").action(statusCommand);
|
|
3761
4071
|
program.command("mcp").description("Run the local MCP server over stdio.").action(mcpCommand);
|
|
4072
|
+
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));
|
|
3762
4073
|
program.command("rename-machine <label>").description('Set the friendly name for THIS machine (e.g. "Office Server", "Travel Mac").').action(renameMachineCommand);
|
|
3763
4074
|
async function main() {
|
|
3764
4075
|
try {
|
|
3765
4076
|
await program.parseAsync(process.argv);
|
|
3766
4077
|
} catch (e) {
|
|
3767
4078
|
if (e instanceof FloomError) {
|
|
3768
|
-
log.err(
|
|
4079
|
+
log.err(e.message);
|
|
3769
4080
|
process.exit(1);
|
|
3770
4081
|
}
|
|
3771
4082
|
log.err(e.message ?? "Unknown error");
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "2.0.
|
|
1
|
+
export const VERSION = "2.0.4";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@floomhq/floom",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
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",
|