@floomhq/skills 0.2.13 → 0.2.18
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 +448 -109
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/dist/index.js.map +0 -7
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(resolve5, reject) {
|
|
115
115
|
_async(function(err, res) {
|
|
116
116
|
if (err) {
|
|
117
117
|
reject(err);
|
|
118
118
|
return;
|
|
119
119
|
}
|
|
120
|
-
|
|
120
|
+
resolve5(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(resolve5, reject) {
|
|
150
150
|
_async(function(err, res) {
|
|
151
151
|
if (err) {
|
|
152
152
|
reject(err);
|
|
153
153
|
return;
|
|
154
154
|
}
|
|
155
|
-
|
|
155
|
+
resolve5(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(resolve5, reject) {
|
|
201
201
|
_async(function(err, res) {
|
|
202
202
|
if (err) {
|
|
203
203
|
reject(err);
|
|
204
204
|
return;
|
|
205
205
|
}
|
|
206
|
-
|
|
206
|
+
resolve5(res);
|
|
207
207
|
});
|
|
208
208
|
});
|
|
209
209
|
};
|
|
@@ -1908,9 +1908,9 @@ function parseManifest(raw) {
|
|
|
1908
1908
|
}
|
|
1909
1909
|
async function readManifest(skillDir) {
|
|
1910
1910
|
const { readFile: readFile11 } = await import("node:fs/promises");
|
|
1911
|
-
const { join:
|
|
1911
|
+
const { join: join16 } = await import("node:path");
|
|
1912
1912
|
try {
|
|
1913
|
-
const raw = await readFile11(
|
|
1913
|
+
const raw = await readFile11(join16(skillDir, "skill.json"), "utf8");
|
|
1914
1914
|
let parsed;
|
|
1915
1915
|
try {
|
|
1916
1916
|
parsed = JSON.parse(raw);
|
|
@@ -2012,8 +2012,9 @@ import { promisify } from "node:util";
|
|
|
2012
2012
|
var scrypt = promisify(scryptCb);
|
|
2013
2013
|
|
|
2014
2014
|
// ../shared/src/install-targets.ts
|
|
2015
|
-
import {
|
|
2016
|
-
import {
|
|
2015
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
2016
|
+
import { homedir, tmpdir } from "node:os";
|
|
2017
|
+
import { basename, dirname, isAbsolute, join, normalize, relative, resolve } from "node:path";
|
|
2017
2018
|
var INSTALL_TARGETS = [
|
|
2018
2019
|
"generic",
|
|
2019
2020
|
"all",
|
|
@@ -2047,10 +2048,36 @@ var TARGET_ENV_DIRS = {
|
|
|
2047
2048
|
opencode: ["FLOOM_SKILLS_DIR", "OPENCODE_SKILLS_DIR", "AGENTS_SKILLS_DIR"],
|
|
2048
2049
|
kimi: ["FLOOM_SKILLS_DIR", "KIMI_SKILLS_DIR", "AGENTS_SKILLS_DIR"]
|
|
2049
2050
|
};
|
|
2051
|
+
var SYSTEM_ROOTS = /* @__PURE__ */ new Set(["/", "/bin", "/etc", "/proc", "/sbin", "/sys", "/usr", "/var"]);
|
|
2052
|
+
function resolveThroughExistingParent(path) {
|
|
2053
|
+
let current = path;
|
|
2054
|
+
while (!existsSync(current)) {
|
|
2055
|
+
const parent = dirname(current);
|
|
2056
|
+
if (parent === current) return path;
|
|
2057
|
+
current = parent;
|
|
2058
|
+
}
|
|
2059
|
+
const realParent = realpathSync.native(current);
|
|
2060
|
+
return resolve(realParent, relative(current, path));
|
|
2061
|
+
}
|
|
2062
|
+
function validateEnvInstallDir(envVar, rawValue) {
|
|
2063
|
+
const trimmed = rawValue.trim();
|
|
2064
|
+
if (!trimmed || trimmed.includes("\0")) {
|
|
2065
|
+
throw new Error(`${envVar} must be a non-empty filesystem path`);
|
|
2066
|
+
}
|
|
2067
|
+
const resolved = resolve(trimmed);
|
|
2068
|
+
const resolvedHome = resolve(homedir());
|
|
2069
|
+
const realTarget = resolveThroughExistingParent(resolved);
|
|
2070
|
+
if (SYSTEM_ROOTS.has(realTarget) || !isWithin(realTarget, resolvedHome)) {
|
|
2071
|
+
throw new Error(
|
|
2072
|
+
`${envVar} points outside your home directory (${realTarget}). Set it to a path under ${resolvedHome} or use --target without an env override.`
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
2075
|
+
return realTarget;
|
|
2076
|
+
}
|
|
2050
2077
|
function envDirForTarget(target) {
|
|
2051
2078
|
for (const key of TARGET_ENV_DIRS[target]) {
|
|
2052
2079
|
const value = process.env[key]?.trim();
|
|
2053
|
-
if (value) return value;
|
|
2080
|
+
if (value) return validateEnvInstallDir(key, value);
|
|
2054
2081
|
}
|
|
2055
2082
|
return null;
|
|
2056
2083
|
}
|
|
@@ -2073,12 +2100,35 @@ function presetDir(target, opts) {
|
|
|
2073
2100
|
return join(root, ".agents", "skills");
|
|
2074
2101
|
}
|
|
2075
2102
|
}
|
|
2103
|
+
function isWithin(child, parent) {
|
|
2104
|
+
const rel = relative(parent, child);
|
|
2105
|
+
return rel === "" || !!rel && !rel.startsWith("..") && !isAbsolute(rel);
|
|
2106
|
+
}
|
|
2107
|
+
function validateExplicitInstallDir(dir, cwd = process.cwd()) {
|
|
2108
|
+
const trimmed = dir.trim();
|
|
2109
|
+
if (!trimmed || trimmed.includes("\0")) {
|
|
2110
|
+
throw new Error("--to must be a non-empty filesystem path");
|
|
2111
|
+
}
|
|
2112
|
+
const absolute = resolve(cwd, trimmed);
|
|
2113
|
+
if (!isAbsolute(trimmed)) {
|
|
2114
|
+
const cwdRoot = resolve(cwd);
|
|
2115
|
+
if (!isWithin(absolute, cwdRoot)) {
|
|
2116
|
+
throw new Error("--to must stay inside the current project when using a relative path");
|
|
2117
|
+
}
|
|
2118
|
+
return normalize(trimmed);
|
|
2119
|
+
}
|
|
2120
|
+
const allowedAbsoluteRoots = [homedir(), tmpdir()].map((root) => resolve(root));
|
|
2121
|
+
if (!allowedAbsoluteRoots.some((root) => isWithin(absolute, root))) {
|
|
2122
|
+
throw new Error("--to absolute paths must be inside your home directory or temporary directory");
|
|
2123
|
+
}
|
|
2124
|
+
return absolute;
|
|
2125
|
+
}
|
|
2076
2126
|
function resolveInstallDir(args) {
|
|
2077
2127
|
const target = args.target ?? "generic";
|
|
2078
2128
|
if (args.to) {
|
|
2079
2129
|
return {
|
|
2080
2130
|
target,
|
|
2081
|
-
dir: args.to,
|
|
2131
|
+
dir: validateExplicitInstallDir(args.to, args.cwd),
|
|
2082
2132
|
origin: "explicit",
|
|
2083
2133
|
compatibleAgents: COMPATIBLE_AGENTS[target]
|
|
2084
2134
|
};
|
|
@@ -2099,6 +2149,13 @@ function resolveInstallDir(args) {
|
|
|
2099
2149
|
compatibleAgents: COMPATIBLE_AGENTS[target]
|
|
2100
2150
|
};
|
|
2101
2151
|
}
|
|
2152
|
+
function normalizeInstallParentDir(parentDir, skillSlug) {
|
|
2153
|
+
const normalized = normalize(parentDir);
|
|
2154
|
+
if (basename(normalized) === skillSlug) {
|
|
2155
|
+
return join(normalized, "..");
|
|
2156
|
+
}
|
|
2157
|
+
return normalized;
|
|
2158
|
+
}
|
|
2102
2159
|
|
|
2103
2160
|
// ../shared/src/security-scan.ts
|
|
2104
2161
|
import { readFile } from "node:fs/promises";
|
|
@@ -2222,7 +2279,7 @@ async function scanSkillBundleFiles(files) {
|
|
|
2222
2279
|
|
|
2223
2280
|
// ../shared/src/skill-package.ts
|
|
2224
2281
|
import { readFile as readFile2, readdir, stat, lstat, mkdir } from "node:fs/promises";
|
|
2225
|
-
import { join as join2, relative, sep, posix } from "node:path";
|
|
2282
|
+
import { join as join2, relative as relative2, sep, posix } from "node:path";
|
|
2226
2283
|
import { createHash as createHash2 } from "node:crypto";
|
|
2227
2284
|
var LIMITS = {
|
|
2228
2285
|
maxBundleBytes: 10 * 1024 * 1024,
|
|
@@ -2278,7 +2335,7 @@ async function walk(rootDir, ignore, acc, current) {
|
|
|
2278
2335
|
const entries = await readdir(current, { withFileTypes: true });
|
|
2279
2336
|
for (const entry of entries) {
|
|
2280
2337
|
const abs = join2(current, entry.name);
|
|
2281
|
-
const rel =
|
|
2338
|
+
const rel = relative2(rootDir, abs).split(sep).join(posix.sep);
|
|
2282
2339
|
if (entry.isSymbolicLink()) {
|
|
2283
2340
|
throw new Error(`Symlinks are not allowed: ${rel}`);
|
|
2284
2341
|
}
|
|
@@ -2347,7 +2404,7 @@ async function extractBundle(buf, destDir) {
|
|
|
2347
2404
|
const tar = await import("tar");
|
|
2348
2405
|
const { Readable } = await import("node:stream");
|
|
2349
2406
|
await mkdir(destDir, { recursive: true });
|
|
2350
|
-
await new Promise((
|
|
2407
|
+
await new Promise((resolve5, reject) => {
|
|
2351
2408
|
const extractStream = tar.extract({
|
|
2352
2409
|
cwd: destDir,
|
|
2353
2410
|
strict: true,
|
|
@@ -2361,7 +2418,7 @@ async function extractBundle(buf, destDir) {
|
|
|
2361
2418
|
if (msg.toLowerCase().includes("symlink")) reject(new Error("Bundle contains symlinks"));
|
|
2362
2419
|
}
|
|
2363
2420
|
});
|
|
2364
|
-
Readable.from(buf).pipe(extractStream).on("finish", () =>
|
|
2421
|
+
Readable.from(buf).pipe(extractStream).on("finish", () => resolve5()).on("error", reject);
|
|
2365
2422
|
});
|
|
2366
2423
|
}
|
|
2367
2424
|
function verifyBundleHash(buf, expected) {
|
|
@@ -2407,7 +2464,11 @@ async function readRawAuth() {
|
|
|
2407
2464
|
const raw = await readFile3(AUTH_FILE, "utf8");
|
|
2408
2465
|
return JSON.parse(raw);
|
|
2409
2466
|
} catch (e) {
|
|
2410
|
-
|
|
2467
|
+
const code = e.code;
|
|
2468
|
+
if (code === "ENOENT") return null;
|
|
2469
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
2470
|
+
throw new FloomError("AUTH_REQUIRED", "Cannot read ~/.floom/auth.json due to file permissions. Fix permissions or run: floom login");
|
|
2471
|
+
}
|
|
2411
2472
|
if (e instanceof SyntaxError) {
|
|
2412
2473
|
throw new FloomError("AUTH_REQUIRED", "Invalid ~/.floom/auth.json. Run: floom login to refresh local auth.");
|
|
2413
2474
|
}
|
|
@@ -2461,14 +2522,17 @@ function allowsCustomApiUrl() {
|
|
|
2461
2522
|
const raw = process.env.FLOOM_ALLOW_CUSTOM_API_URL?.trim().toLowerCase();
|
|
2462
2523
|
return raw === "1" || raw === "true" || raw === "yes";
|
|
2463
2524
|
}
|
|
2464
|
-
function
|
|
2525
|
+
function isCanonicalApiUrl(apiUrl) {
|
|
2465
2526
|
try {
|
|
2466
2527
|
const url = new URL(apiUrl);
|
|
2467
|
-
return TRUSTED_API_HOSTS.has(url.hostname)
|
|
2528
|
+
return TRUSTED_API_HOSTS.has(url.hostname);
|
|
2468
2529
|
} catch {
|
|
2469
2530
|
return false;
|
|
2470
2531
|
}
|
|
2471
2532
|
}
|
|
2533
|
+
function isTrustedApiUrl(apiUrl) {
|
|
2534
|
+
return isCanonicalApiUrl(apiUrl) || allowsCustomApiUrl();
|
|
2535
|
+
}
|
|
2472
2536
|
function trustedApiUrlOrDefault(apiUrl) {
|
|
2473
2537
|
const normalized = normalizeApiUrl(apiUrl);
|
|
2474
2538
|
return isTrustedApiUrl(normalized) ? normalized : DEFAULT_API_URL;
|
|
@@ -2483,11 +2547,12 @@ function isLegacyApiUrl(apiUrl) {
|
|
|
2483
2547
|
}
|
|
2484
2548
|
|
|
2485
2549
|
// src/version.ts
|
|
2486
|
-
var VERSION = "0.2.
|
|
2550
|
+
var VERSION = "0.2.18";
|
|
2487
2551
|
|
|
2488
2552
|
// src/api-client.ts
|
|
2489
2553
|
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
2490
2554
|
var DEFAULT_RETRY_ATTEMPTS = 2;
|
|
2555
|
+
var MAX_ERROR_BODY_BYTES = 64 * 1024;
|
|
2491
2556
|
function timeoutMs() {
|
|
2492
2557
|
const raw = Number(process.env.FLOOM_API_TIMEOUT_MS);
|
|
2493
2558
|
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_TIMEOUT_MS;
|
|
@@ -2503,7 +2568,7 @@ function retryDelayMs(attempt) {
|
|
|
2503
2568
|
}
|
|
2504
2569
|
async function sleep(ms) {
|
|
2505
2570
|
if (ms <= 0) return;
|
|
2506
|
-
await new Promise((
|
|
2571
|
+
await new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
2507
2572
|
}
|
|
2508
2573
|
async function fetchWithTimeout(url, init = {}) {
|
|
2509
2574
|
const controller = new AbortController();
|
|
@@ -2519,12 +2584,32 @@ async function fetchWithTimeout(url, init = {}) {
|
|
|
2519
2584
|
clearTimeout(timer);
|
|
2520
2585
|
}
|
|
2521
2586
|
}
|
|
2522
|
-
function
|
|
2523
|
-
|
|
2524
|
-
|
|
2587
|
+
async function readLimitedText(res, limitBytes = MAX_ERROR_BODY_BYTES) {
|
|
2588
|
+
if (!res.body) return res.text();
|
|
2589
|
+
const reader = res.body.getReader();
|
|
2590
|
+
const chunks = [];
|
|
2591
|
+
let total = 0;
|
|
2592
|
+
let truncated = false;
|
|
2593
|
+
while (true) {
|
|
2594
|
+
const { done, value } = await reader.read();
|
|
2595
|
+
if (done) break;
|
|
2596
|
+
const chunk = value ?? new Uint8Array();
|
|
2597
|
+
if (total + chunk.byteLength > limitBytes) {
|
|
2598
|
+
const remaining = Math.max(limitBytes - total, 0);
|
|
2599
|
+
if (remaining > 0) chunks.push(chunk.slice(0, remaining));
|
|
2600
|
+
truncated = true;
|
|
2601
|
+
await reader.cancel();
|
|
2602
|
+
break;
|
|
2603
|
+
}
|
|
2604
|
+
chunks.push(chunk);
|
|
2605
|
+
total += chunk.byteLength;
|
|
2606
|
+
}
|
|
2607
|
+
const text = new TextDecoder().decode(Buffer.concat(chunks));
|
|
2608
|
+
return truncated ? `${text}
|
|
2609
|
+
[truncated]` : text;
|
|
2525
2610
|
}
|
|
2526
2611
|
function isRedirect(res) {
|
|
2527
|
-
return res.status >= 300 && res.status < 400;
|
|
2612
|
+
return res.type === "opaqueredirect" || res.status >= 300 && res.status < 400;
|
|
2528
2613
|
}
|
|
2529
2614
|
function isRetryableStatus(status) {
|
|
2530
2615
|
return status === 408 || status === 429 || status >= 500;
|
|
@@ -2534,8 +2619,9 @@ function isRetryableMethod(method) {
|
|
|
2534
2619
|
}
|
|
2535
2620
|
async function api(path, opts = {}) {
|
|
2536
2621
|
const auth = await readAuth();
|
|
2537
|
-
const
|
|
2538
|
-
|
|
2622
|
+
const envToken = process.env.FLOOM_API_TOKEN?.trim();
|
|
2623
|
+
const storedToken = auth?.token;
|
|
2624
|
+
if (opts.authRequired && !opts.tokenOverride && !envToken && !storedToken) {
|
|
2539
2625
|
throw new FloomError("AUTH_REQUIRED", "Not logged in. Run: floom login");
|
|
2540
2626
|
}
|
|
2541
2627
|
let lastError = null;
|
|
@@ -2555,7 +2641,15 @@ async function api(path, opts = {}) {
|
|
|
2555
2641
|
"User-Agent": `floom-cli/${VERSION}`,
|
|
2556
2642
|
"x-floom-cli-version": VERSION
|
|
2557
2643
|
};
|
|
2558
|
-
const requestToken = opts.tokenOverride ??
|
|
2644
|
+
const requestToken = opts.tokenOverride ?? envToken ?? (isCanonicalApiUrl(base) ? storedToken : void 0);
|
|
2645
|
+
if (opts.authRequired && !requestToken) {
|
|
2646
|
+
lastError = new FloomError(
|
|
2647
|
+
"AUTH_REQUIRED",
|
|
2648
|
+
"Refusing to send stored auth token to a custom API URL. Set FLOOM_API_TOKEN explicitly only if you trust that API.",
|
|
2649
|
+
{ apiUrl: base }
|
|
2650
|
+
);
|
|
2651
|
+
break;
|
|
2652
|
+
}
|
|
2559
2653
|
if (requestToken) headers.Authorization = `Bearer ${requestToken}`;
|
|
2560
2654
|
let res;
|
|
2561
2655
|
try {
|
|
@@ -2580,17 +2674,25 @@ async function api(path, opts = {}) {
|
|
|
2580
2674
|
throw new FloomError(
|
|
2581
2675
|
"INTERNAL_ERROR",
|
|
2582
2676
|
"Floom API returned an unexpected redirect. Refusing to forward credentials.",
|
|
2583
|
-
{ status: res.status, apiUrl: base
|
|
2677
|
+
{ status: res.status, apiUrl: base }
|
|
2584
2678
|
);
|
|
2585
2679
|
}
|
|
2586
|
-
const text = await res.text();
|
|
2680
|
+
const text = res.ok ? await res.text() : await readLimitedText(res);
|
|
2587
2681
|
let json = null;
|
|
2588
2682
|
try {
|
|
2589
2683
|
json = text ? JSON.parse(text) : null;
|
|
2590
2684
|
} catch {
|
|
2591
2685
|
}
|
|
2686
|
+
const envelopeError = json?.error;
|
|
2687
|
+
if (res.ok && envelopeError && typeof envelopeError === "object" && envelopeError.code) {
|
|
2688
|
+
throw new FloomError(
|
|
2689
|
+
envelopeError.code,
|
|
2690
|
+
typeof envelopeError.message === "string" ? envelopeError.message : String(envelopeError.code),
|
|
2691
|
+
{ status: res.status, requestId: envelopeError.request_id, apiUrl: base }
|
|
2692
|
+
);
|
|
2693
|
+
}
|
|
2592
2694
|
if (res.ok) return json;
|
|
2593
|
-
const err =
|
|
2695
|
+
const err = envelopeError ?? {};
|
|
2594
2696
|
lastError = new FloomError(
|
|
2595
2697
|
err.code ?? "INTERNAL_ERROR",
|
|
2596
2698
|
err.message ?? `HTTP ${res.status} ${res.statusText}`,
|
|
@@ -2621,7 +2723,7 @@ async function rawPut(url, body, contentType = "application/octet-stream") {
|
|
|
2621
2723
|
if (isRedirect(res)) {
|
|
2622
2724
|
throw new FloomError(
|
|
2623
2725
|
"UPLOAD_FAILED",
|
|
2624
|
-
|
|
2726
|
+
"Upload failed: unexpected redirect."
|
|
2625
2727
|
);
|
|
2626
2728
|
}
|
|
2627
2729
|
if (!res.ok) {
|
|
@@ -2641,7 +2743,7 @@ async function rawGet(url) {
|
|
|
2641
2743
|
if (isRedirect(res)) {
|
|
2642
2744
|
throw new FloomError(
|
|
2643
2745
|
"DOWNLOAD_FAILED",
|
|
2644
|
-
|
|
2746
|
+
"Download failed: unexpected redirect."
|
|
2645
2747
|
);
|
|
2646
2748
|
}
|
|
2647
2749
|
if (!res.ok) {
|
|
@@ -2685,9 +2787,18 @@ async function loginCommand() {
|
|
|
2685
2787
|
const interval = Math.max(2, session.poll_interval_seconds) * 1e3;
|
|
2686
2788
|
while (Date.now() < deadline) {
|
|
2687
2789
|
await new Promise((r) => setTimeout(r, interval));
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2790
|
+
let poll;
|
|
2791
|
+
try {
|
|
2792
|
+
poll = await api(`/cli/sessions/${session.session_id}`, {
|
|
2793
|
+
tokenOverride: session.device_code
|
|
2794
|
+
});
|
|
2795
|
+
} catch (e) {
|
|
2796
|
+
if (e instanceof FloomError && String(e.code) === "DEVICE_PENDING") {
|
|
2797
|
+
process.stdout.write(".");
|
|
2798
|
+
continue;
|
|
2799
|
+
}
|
|
2800
|
+
throw e;
|
|
2801
|
+
}
|
|
2691
2802
|
if (poll.status === "approved" && poll.token && poll.handle && poll.email) {
|
|
2692
2803
|
await writeAuth({
|
|
2693
2804
|
token: poll.token,
|
|
@@ -2759,13 +2870,16 @@ async function whoamiCommand() {
|
|
|
2759
2870
|
log.heading("Logged in as:");
|
|
2760
2871
|
log.kv("handle", `@${me.user.handle}`);
|
|
2761
2872
|
log.kv("email", me.user.email);
|
|
2873
|
+
if (me.workspace) {
|
|
2874
|
+
log.kv("workspace", `${me.workspace.slug} (${me.workspace.role})`);
|
|
2875
|
+
}
|
|
2762
2876
|
log.kv("api url", process.env.FLOOM_API_URL ?? auth?.apiUrl ?? "default");
|
|
2763
2877
|
if (envToken) log.kv("auth", "FLOOM_API_TOKEN");
|
|
2764
2878
|
}
|
|
2765
2879
|
|
|
2766
2880
|
// src/commands/init.ts
|
|
2767
2881
|
import { writeFile as writeFile2, stat as stat2 } from "node:fs/promises";
|
|
2768
|
-
import { join as join4, basename } from "node:path";
|
|
2882
|
+
import { join as join4, basename as basename2 } from "node:path";
|
|
2769
2883
|
import prompts from "prompts";
|
|
2770
2884
|
async function pathExists(p) {
|
|
2771
2885
|
try {
|
|
@@ -2777,7 +2891,7 @@ async function pathExists(p) {
|
|
|
2777
2891
|
}
|
|
2778
2892
|
async function initCommand() {
|
|
2779
2893
|
const cwd = process.cwd();
|
|
2780
|
-
const folderName =
|
|
2894
|
+
const folderName = basename2(cwd);
|
|
2781
2895
|
if (await pathExists(join4(cwd, "skill.json"))) {
|
|
2782
2896
|
log.err("skill.json already exists in this folder. Edit it directly or run from a fresh directory.");
|
|
2783
2897
|
process.exit(1);
|
|
@@ -2979,15 +3093,52 @@ async function validateCommand(opts = {}) {
|
|
|
2979
3093
|
// src/commands/publish.ts
|
|
2980
3094
|
import { readFile as readFile6 } from "node:fs/promises";
|
|
2981
3095
|
import { join as join6 } from "node:path";
|
|
3096
|
+
|
|
3097
|
+
// src/lib/publish-urls.ts
|
|
3098
|
+
function trimAppUrl(appUrl) {
|
|
3099
|
+
return appUrl.replace(/\/api\/v1\/?$/, "").replace(/\/$/, "");
|
|
3100
|
+
}
|
|
2982
3101
|
function buildAuthenticatedSkillUrl(appUrl, librarySlug, skillSlug) {
|
|
2983
|
-
const base = appUrl
|
|
3102
|
+
const base = trimAppUrl(appUrl);
|
|
2984
3103
|
return `${base}/library/${encodeURIComponent(skillSlug)}?lib=${encodeURIComponent(librarySlug)}`;
|
|
2985
3104
|
}
|
|
2986
3105
|
function buildPublicSkillUrl(appUrl, handle, librarySlug, skillSlug) {
|
|
2987
|
-
const base = appUrl
|
|
3106
|
+
const base = trimAppUrl(appUrl);
|
|
2988
3107
|
if (librarySlug !== handle) return null;
|
|
2989
3108
|
return `${base}/@${handle}/${skillSlug}`;
|
|
2990
3109
|
}
|
|
3110
|
+
function buildPublishViewLines(args) {
|
|
3111
|
+
const manageUrl = buildAuthenticatedSkillUrl(args.appUrl, args.refRoot, args.slug);
|
|
3112
|
+
const publicUrl = buildPublicSkillUrl(args.appUrl, args.handle, args.refRoot, args.slug);
|
|
3113
|
+
if (args.visibility === "public") {
|
|
3114
|
+
return {
|
|
3115
|
+
heading: publicUrl ? "View (public):" : "Manage (workspace skill):",
|
|
3116
|
+
primaryUrl: publicUrl ?? manageUrl,
|
|
3117
|
+
...publicUrl ? {} : { note: "Workspace skills stay under their authenticated library URL unless copied to your personal library." },
|
|
3118
|
+
shareUrl: args.shareUrl ?? void 0
|
|
3119
|
+
};
|
|
3120
|
+
}
|
|
3121
|
+
if (args.visibility === "unlisted") {
|
|
3122
|
+
return {
|
|
3123
|
+
heading: publicUrl ? "View (unlisted - use a share link for unauthenticated access):" : "Manage (unlisted workspace skill):",
|
|
3124
|
+
primaryUrl: publicUrl ?? manageUrl,
|
|
3125
|
+
...publicUrl ? {} : { note: "Use the share link for unauthenticated access; workspace detail URLs remain authenticated." },
|
|
3126
|
+
shareUrl: args.shareUrl ?? void 0
|
|
3127
|
+
};
|
|
3128
|
+
}
|
|
3129
|
+
return {
|
|
3130
|
+
heading: "Manage (private - sign in to your workspace):",
|
|
3131
|
+
primaryUrl: manageUrl,
|
|
3132
|
+
...publicUrl ? {
|
|
3133
|
+
secondaryHeading: "Public URL after you make it public or unlisted:",
|
|
3134
|
+
secondaryUrl: publicUrl
|
|
3135
|
+
} : {
|
|
3136
|
+
note: "Workspace skills stay under their authenticated library URL unless copied to your personal library."
|
|
3137
|
+
}
|
|
3138
|
+
};
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
// src/commands/publish.ts
|
|
2991
3142
|
async function publishCommand(opts = {}) {
|
|
2992
3143
|
const auth = await readAuth();
|
|
2993
3144
|
const envToken = process.env.FLOOM_API_TOKEN?.trim();
|
|
@@ -2995,7 +3146,7 @@ async function publishCommand(opts = {}) {
|
|
|
2995
3146
|
log.err("Not logged in. Run: floom login");
|
|
2996
3147
|
process.exit(1);
|
|
2997
3148
|
}
|
|
2998
|
-
const dir = process.cwd();
|
|
3149
|
+
const dir = opts.dir ?? process.cwd();
|
|
2999
3150
|
log.heading("Validating skill...");
|
|
3000
3151
|
const report = await validateSkill(dir);
|
|
3001
3152
|
if (!report.ok) {
|
|
@@ -3038,6 +3189,7 @@ async function publishCommand(opts = {}) {
|
|
|
3038
3189
|
authRequired: true,
|
|
3039
3190
|
body: {
|
|
3040
3191
|
manifest,
|
|
3192
|
+
folder_id: opts.folder,
|
|
3041
3193
|
skill_md: skillMd,
|
|
3042
3194
|
bundle: {
|
|
3043
3195
|
sha256: packed.sha256,
|
|
@@ -3070,34 +3222,126 @@ async function publishCommand(opts = {}) {
|
|
|
3070
3222
|
log.blank();
|
|
3071
3223
|
log.ok(`Published ${complete.ref}`);
|
|
3072
3224
|
log.blank();
|
|
3073
|
-
log.info("Manage:");
|
|
3074
3225
|
const displayApiUrl = trustedApiUrlOrDefault(process.env.FLOOM_API_URL ?? auth?.apiUrl ?? DEFAULT_API_URL);
|
|
3075
|
-
|
|
3076
|
-
const
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3226
|
+
const appUrl = displayApiUrl.replace(/\/api\/v1\/?$/, "");
|
|
3227
|
+
const visibility = complete.visibility ?? "private";
|
|
3228
|
+
const views = buildPublishViewLines({
|
|
3229
|
+
visibility,
|
|
3230
|
+
appUrl,
|
|
3231
|
+
handle,
|
|
3232
|
+
slug: manifest.name,
|
|
3233
|
+
refRoot,
|
|
3234
|
+
shareUrl: complete.share_url
|
|
3235
|
+
});
|
|
3236
|
+
log.info(views.heading);
|
|
3237
|
+
log.kv("", views.primaryUrl);
|
|
3238
|
+
if (views.secondaryHeading && views.secondaryUrl) {
|
|
3239
|
+
log.info(views.secondaryHeading);
|
|
3240
|
+
log.kv("", views.secondaryUrl);
|
|
3241
|
+
}
|
|
3242
|
+
if (views.note) {
|
|
3243
|
+
log.info(views.note);
|
|
3244
|
+
}
|
|
3245
|
+
if (views.shareUrl) {
|
|
3246
|
+
log.info("Share link:");
|
|
3247
|
+
log.kv("", views.shareUrl);
|
|
3248
|
+
} else if (visibility === "unlisted") {
|
|
3249
|
+
log.info(`Create a share link: floom link create ${refRoot}/${manifest.name}`);
|
|
3083
3250
|
}
|
|
3084
3251
|
log.info("Install:");
|
|
3085
3252
|
log.kv("", complete.install_command);
|
|
3086
3253
|
}
|
|
3087
3254
|
|
|
3255
|
+
// src/commands/push.ts
|
|
3256
|
+
import { readdir as readdir2, stat as stat3 } from "node:fs/promises";
|
|
3257
|
+
import { basename as basename3, join as join7, resolve as resolve2 } from "node:path";
|
|
3258
|
+
function parsePushConcurrency(value) {
|
|
3259
|
+
const raw = value ?? 6;
|
|
3260
|
+
const parsed = typeof raw === "number" ? raw : Number.parseInt(raw, 10);
|
|
3261
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 16) {
|
|
3262
|
+
throw new Error("--concurrency must be an integer from 1 to 16.");
|
|
3263
|
+
}
|
|
3264
|
+
return parsed;
|
|
3265
|
+
}
|
|
3266
|
+
async function hasSkillFiles(dir) {
|
|
3267
|
+
try {
|
|
3268
|
+
const [skillMd, manifest] = await Promise.all([
|
|
3269
|
+
stat3(join7(dir, "SKILL.md")),
|
|
3270
|
+
stat3(join7(dir, "skill.json"))
|
|
3271
|
+
]);
|
|
3272
|
+
return skillMd.isFile() && manifest.isFile();
|
|
3273
|
+
} catch {
|
|
3274
|
+
return false;
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
async function findImmediateSkillDirs(root) {
|
|
3278
|
+
const entries = await readdir2(root, { withFileTypes: true });
|
|
3279
|
+
const dirs = entries.filter((entry) => entry.isDirectory()).map((entry) => join7(root, entry.name)).sort();
|
|
3280
|
+
const checks = await Promise.all(dirs.map(async (dir) => await hasSkillFiles(dir) ? dir : null));
|
|
3281
|
+
return checks.filter((dir) => Boolean(dir));
|
|
3282
|
+
}
|
|
3283
|
+
async function runBounded(items, concurrency, worker) {
|
|
3284
|
+
let next = 0;
|
|
3285
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
3286
|
+
while (next < items.length) {
|
|
3287
|
+
const item = items[next];
|
|
3288
|
+
next += 1;
|
|
3289
|
+
await worker(item);
|
|
3290
|
+
}
|
|
3291
|
+
});
|
|
3292
|
+
await Promise.all(workers);
|
|
3293
|
+
}
|
|
3294
|
+
async function pushCommand(dir = ".", options = {}, deps = {}) {
|
|
3295
|
+
const root = resolve2(dir);
|
|
3296
|
+
const rootStat = await stat3(root).catch(() => null);
|
|
3297
|
+
if (!rootStat?.isDirectory()) {
|
|
3298
|
+
throw new Error(`Directory not found: ${dir}`);
|
|
3299
|
+
}
|
|
3300
|
+
const publish = deps.publish ?? publishCommand;
|
|
3301
|
+
if (await hasSkillFiles(root)) {
|
|
3302
|
+
await publish({ ...options, dir: root });
|
|
3303
|
+
return;
|
|
3304
|
+
}
|
|
3305
|
+
const skillDirs = await findImmediateSkillDirs(root);
|
|
3306
|
+
if (skillDirs.length === 0) {
|
|
3307
|
+
throw new Error("SKILL.md and skill.json are required, either in this directory or immediate child directories.");
|
|
3308
|
+
}
|
|
3309
|
+
const concurrency = parsePushConcurrency(options.concurrency);
|
|
3310
|
+
const startedAt = Date.now();
|
|
3311
|
+
const errors = [];
|
|
3312
|
+
let pushed = 0;
|
|
3313
|
+
await runBounded(skillDirs, concurrency, async (skillDir) => {
|
|
3314
|
+
const slug = basename3(skillDir);
|
|
3315
|
+
try {
|
|
3316
|
+
await publish({ ...options, dir: skillDir });
|
|
3317
|
+
pushed += 1;
|
|
3318
|
+
log.info(`Pushed ${slug}`);
|
|
3319
|
+
} catch (error) {
|
|
3320
|
+
errors.push({ slug, message: error.message });
|
|
3321
|
+
}
|
|
3322
|
+
});
|
|
3323
|
+
const elapsed = ((Date.now() - startedAt) / 1e3).toFixed(1).replace(/\.0$/, "");
|
|
3324
|
+
log.info(`Pushed ${pushed}/${skillDirs.length} skills in ${elapsed}s.`);
|
|
3325
|
+
if (errors.length > 0) {
|
|
3326
|
+
log.err("Push errors:");
|
|
3327
|
+
for (const error of errors) log.err(`- ${error.slug}: ${error.message}`);
|
|
3328
|
+
process.exitCode = 1;
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3088
3332
|
// src/commands/install.ts
|
|
3089
|
-
import { mkdir as mkdir4, readdir as
|
|
3090
|
-
import { join as
|
|
3091
|
-
import { tmpdir } from "node:os";
|
|
3333
|
+
import { mkdir as mkdir4, readdir as readdir4, rm, rename } from "node:fs/promises";
|
|
3334
|
+
import { join as join9 } from "node:path";
|
|
3335
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
3092
3336
|
|
|
3093
3337
|
// src/lib/floom-lock.ts
|
|
3094
|
-
import { readFile as readFile7, writeFile as writeFile3, mkdir as mkdir3, stat as
|
|
3095
|
-
import { join as
|
|
3338
|
+
import { readFile as readFile7, writeFile as writeFile3, mkdir as mkdir3, stat as stat4, readdir as readdir3 } from "node:fs/promises";
|
|
3339
|
+
import { join as join8, relative as relative3, sep as sep2, posix as posix2 } from "node:path";
|
|
3096
3340
|
import { createHash as createHash3 } from "node:crypto";
|
|
3097
3341
|
var EMPTY = { schema_version: "0.1", skills: {} };
|
|
3098
3342
|
async function readLock(projectDir) {
|
|
3099
3343
|
try {
|
|
3100
|
-
const raw = await readFile7(
|
|
3344
|
+
const raw = await readFile7(join8(projectDir, "floom.lock"), "utf8");
|
|
3101
3345
|
const parsed = JSON.parse(raw);
|
|
3102
3346
|
if (parsed.schema_version === "0.1") return parsed;
|
|
3103
3347
|
if (parsed.schema_version === "0.2") return parsed;
|
|
@@ -3112,7 +3356,7 @@ async function readLock(projectDir) {
|
|
|
3112
3356
|
}
|
|
3113
3357
|
async function writeLock(projectDir, lock) {
|
|
3114
3358
|
await mkdir3(projectDir, { recursive: true });
|
|
3115
|
-
await writeFile3(
|
|
3359
|
+
await writeFile3(join8(projectDir, "floom.lock"), JSON.stringify(lock, null, 2) + "\n", "utf8");
|
|
3116
3360
|
}
|
|
3117
3361
|
function setLockEntry(lock, ref, entry) {
|
|
3118
3362
|
return { ...lock, skills: { ...lock.skills, [ref]: entry } };
|
|
@@ -3142,12 +3386,12 @@ async function hashInstalledFolder(folderAbs) {
|
|
|
3142
3386
|
async function walk2(dir) {
|
|
3143
3387
|
let entries;
|
|
3144
3388
|
try {
|
|
3145
|
-
entries = await
|
|
3389
|
+
entries = await readdir3(dir, { withFileTypes: true });
|
|
3146
3390
|
} catch {
|
|
3147
3391
|
return;
|
|
3148
3392
|
}
|
|
3149
3393
|
for (const entry of entries) {
|
|
3150
|
-
const abs =
|
|
3394
|
+
const abs = join8(dir, entry.name);
|
|
3151
3395
|
if (entry.isSymbolicLink()) continue;
|
|
3152
3396
|
if (entry.isDirectory()) {
|
|
3153
3397
|
await walk2(abs);
|
|
@@ -3155,7 +3399,7 @@ async function hashInstalledFolder(folderAbs) {
|
|
|
3155
3399
|
}
|
|
3156
3400
|
if (!entry.isFile()) continue;
|
|
3157
3401
|
const buf = await readFile7(abs);
|
|
3158
|
-
const rel =
|
|
3402
|
+
const rel = relative3(folderAbs, abs).split(sep2).join(posix2.sep);
|
|
3159
3403
|
out.push({
|
|
3160
3404
|
relPath: rel,
|
|
3161
3405
|
sha256: createHash3("sha256").update(buf).digest("hex"),
|
|
@@ -3164,7 +3408,7 @@ async function hashInstalledFolder(folderAbs) {
|
|
|
3164
3408
|
}
|
|
3165
3409
|
}
|
|
3166
3410
|
try {
|
|
3167
|
-
await
|
|
3411
|
+
await stat4(folderAbs);
|
|
3168
3412
|
} catch {
|
|
3169
3413
|
return out;
|
|
3170
3414
|
}
|
|
@@ -3173,6 +3417,20 @@ async function hashInstalledFolder(folderAbs) {
|
|
|
3173
3417
|
return out;
|
|
3174
3418
|
}
|
|
3175
3419
|
|
|
3420
|
+
// src/lib/filesystem-safety.ts
|
|
3421
|
+
import { lstat as lstat2 } from "node:fs/promises";
|
|
3422
|
+
async function assertPathIsNotSymlink(path, label = "path") {
|
|
3423
|
+
try {
|
|
3424
|
+
const stats = await lstat2(path);
|
|
3425
|
+
if (stats.isSymbolicLink()) {
|
|
3426
|
+
throw new Error(`${label} must not be a symbolic link: ${path}`);
|
|
3427
|
+
}
|
|
3428
|
+
} catch (e) {
|
|
3429
|
+
if (e.code === "ENOENT") return;
|
|
3430
|
+
throw e;
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3176
3434
|
// src/commands/install.ts
|
|
3177
3435
|
var CLI_VERSION = VERSION;
|
|
3178
3436
|
function semverGte(a, b) {
|
|
@@ -3226,13 +3484,16 @@ async function installCommand(refStr, opts = {}) {
|
|
|
3226
3484
|
global: opts.global
|
|
3227
3485
|
});
|
|
3228
3486
|
const projectDir = opts.global ? process.cwd() : process.cwd();
|
|
3229
|
-
const
|
|
3487
|
+
const parentDir = opts.to ? normalizeInstallParentDir(target.dir, ref.slug) : target.dir;
|
|
3488
|
+
const destFolder = join9(parentDir, ref.slug);
|
|
3489
|
+
await assertPathIsNotSymlink(parentDir, "install parent directory");
|
|
3490
|
+
await assertPathIsNotSymlink(destFolder, "install destination");
|
|
3230
3491
|
const lock = await readLock(projectDir);
|
|
3231
3492
|
const refKey = formatSkillRef({ owner: ref.owner, slug: ref.slug });
|
|
3232
3493
|
const existing = lock.skills[refKey];
|
|
3233
3494
|
let folderExists = false;
|
|
3234
3495
|
try {
|
|
3235
|
-
const entries = await
|
|
3496
|
+
const entries = await readdir4(destFolder);
|
|
3236
3497
|
folderExists = entries.length > 0;
|
|
3237
3498
|
} catch {
|
|
3238
3499
|
}
|
|
@@ -3262,7 +3523,7 @@ async function installCommand(refStr, opts = {}) {
|
|
|
3262
3523
|
}
|
|
3263
3524
|
log.ok(`Bundle verified (${(buf.length / 1024).toFixed(1)} KB).`);
|
|
3264
3525
|
const tmp = await import("node:fs/promises").then(
|
|
3265
|
-
(m) => m.mkdtemp(
|
|
3526
|
+
(m) => m.mkdtemp(join9(tmpdir2(), `floom-install-${ref.slug}-`))
|
|
3266
3527
|
);
|
|
3267
3528
|
try {
|
|
3268
3529
|
await extractBundle(buf, tmp);
|
|
@@ -3365,8 +3626,8 @@ async function outdatedCommand() {
|
|
|
3365
3626
|
|
|
3366
3627
|
// src/commands/update.ts
|
|
3367
3628
|
import { rm as rm2, rename as rename2, mkdir as mkdir5 } from "node:fs/promises";
|
|
3368
|
-
import { tmpdir as
|
|
3369
|
-
import { join as
|
|
3629
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
3630
|
+
import { join as join10, resolve as resolve4 } from "node:path";
|
|
3370
3631
|
function cmpSemver2(a, b) {
|
|
3371
3632
|
const pa = a.split(".").map((p) => parseInt(p, 10));
|
|
3372
3633
|
const pb = b.split(".").map((p) => parseInt(p, 10));
|
|
@@ -3405,7 +3666,8 @@ async function updateCommand(refStr, opts = {}) {
|
|
|
3405
3666
|
log.step(`${ref} is already at latest (${entry.version}).`);
|
|
3406
3667
|
continue;
|
|
3407
3668
|
}
|
|
3408
|
-
const installDir =
|
|
3669
|
+
const installDir = resolve4(projectDir, entry.path);
|
|
3670
|
+
await assertPathIsNotSymlink(installDir, "installed skill directory");
|
|
3409
3671
|
if (!opts.force) {
|
|
3410
3672
|
const current = await hashInstalledFolder(installDir);
|
|
3411
3673
|
log.step(`Updating ${ref}: ${entry.version} -> ${info.latest_version}`);
|
|
@@ -3418,7 +3680,7 @@ async function updateCommand(refStr, opts = {}) {
|
|
|
3418
3680
|
continue;
|
|
3419
3681
|
}
|
|
3420
3682
|
const tmp = await import("node:fs/promises").then(
|
|
3421
|
-
(m) => m.mkdtemp(
|
|
3683
|
+
(m) => m.mkdtemp(join10(tmpdir3(), `floom-update-${slug}-`))
|
|
3422
3684
|
);
|
|
3423
3685
|
try {
|
|
3424
3686
|
await extractBundle(buf, tmp);
|
|
@@ -3494,6 +3756,8 @@ async function infoCommand(refStr) {
|
|
|
3494
3756
|
log.kv("Latest", r.latest_version);
|
|
3495
3757
|
log.kv("Visibility", r.visibility);
|
|
3496
3758
|
log.kv("Owner", `@${r.owner.handle}${r.owner.display_name ? ` (${r.owner.display_name})` : ""}`);
|
|
3759
|
+
if (r.folder) log.kv("Folder", `${r.folder.name} (${r.folder.slug}, ${r.folder.access_mode})`);
|
|
3760
|
+
if (r.tags?.length) log.kv("Tags", r.tags.map((tag) => `#${tag}`).join(" "));
|
|
3497
3761
|
if (r.has_scripts) {
|
|
3498
3762
|
log.kv("Scripts", "yes (review before activating)");
|
|
3499
3763
|
}
|
|
@@ -3545,6 +3809,26 @@ async function unshareCommand(refStr, email) {
|
|
|
3545
3809
|
log.ok(`Removed ${email}'s access to ${refStr}.`);
|
|
3546
3810
|
}
|
|
3547
3811
|
|
|
3812
|
+
// src/commands/link.ts
|
|
3813
|
+
async function linkCreateCommand(refStr, opts = {}) {
|
|
3814
|
+
const ref = parseSkillRef(refStr);
|
|
3815
|
+
if (!ref) {
|
|
3816
|
+
log.err(`Invalid skill ref: ${refStr}`);
|
|
3817
|
+
process.exit(1);
|
|
3818
|
+
}
|
|
3819
|
+
const role = opts.role ?? "viewer";
|
|
3820
|
+
const body = { role };
|
|
3821
|
+
if (opts.name) body.name = opts.name;
|
|
3822
|
+
const r = await api(`/skills/${ref.owner}/${ref.slug}/links`, {
|
|
3823
|
+
method: "POST",
|
|
3824
|
+
authRequired: true,
|
|
3825
|
+
body
|
|
3826
|
+
});
|
|
3827
|
+
log.ok(`Share link created for ${refStr}.`);
|
|
3828
|
+
log.kv("url", r.link.url);
|
|
3829
|
+
log.kv("role", r.link.role);
|
|
3830
|
+
}
|
|
3831
|
+
|
|
3548
3832
|
// src/commands/library.ts
|
|
3549
3833
|
function isValidEmail2(email) {
|
|
3550
3834
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
@@ -3614,7 +3898,7 @@ async function workspaceAcceptCommand(tokenOrUrl) {
|
|
|
3614
3898
|
// src/lib/config-file.ts
|
|
3615
3899
|
import { mkdir as mkdir6, readFile as readFile8, writeFile as writeFile4 } from "node:fs/promises";
|
|
3616
3900
|
import { homedir as homedir3 } from "node:os";
|
|
3617
|
-
import { dirname, join as
|
|
3901
|
+
import { dirname as dirname2, join as join11 } from "node:path";
|
|
3618
3902
|
import { z as z2 } from "zod";
|
|
3619
3903
|
var FLOOM_CONFIG_VERSION = "0.1";
|
|
3620
3904
|
var TARGETS = ["claude", "codex", "cursor", "kimi", "opencode"];
|
|
@@ -3628,8 +3912,8 @@ var configSchema = z2.object({
|
|
|
3628
3912
|
targets: z2.record(z2.enum(TARGETS), targetConfigSchema).default({})
|
|
3629
3913
|
});
|
|
3630
3914
|
function configPath(scope, cwd = process.cwd()) {
|
|
3631
|
-
if (scope === "global") return
|
|
3632
|
-
return
|
|
3915
|
+
if (scope === "global") return join11(homedir3(), ".floom", "config.json");
|
|
3916
|
+
return join11(cwd, ".floom", "config.json");
|
|
3633
3917
|
}
|
|
3634
3918
|
async function readFloomConfig(scope, cwd = process.cwd()) {
|
|
3635
3919
|
try {
|
|
@@ -3643,7 +3927,7 @@ async function readFloomConfig(scope, cwd = process.cwd()) {
|
|
|
3643
3927
|
async function writeFloomConfig(scope, config, cwd = process.cwd()) {
|
|
3644
3928
|
const path = configPath(scope, cwd);
|
|
3645
3929
|
const parsed = configSchema.parse(config);
|
|
3646
|
-
await mkdir6(
|
|
3930
|
+
await mkdir6(dirname2(path), { recursive: true, mode: 448 });
|
|
3647
3931
|
await writeFile4(path, JSON.stringify(parsed, null, 2) + "\n", { mode: 384 });
|
|
3648
3932
|
}
|
|
3649
3933
|
function normalizeScope(value) {
|
|
@@ -3681,7 +3965,7 @@ async function setWorkspaceActive(input) {
|
|
|
3681
3965
|
|
|
3682
3966
|
// src/commands/instruction.ts
|
|
3683
3967
|
import { mkdir as mkdir7, readFile as readFile9, writeFile as writeFile5 } from "node:fs/promises";
|
|
3684
|
-
import { basename as
|
|
3968
|
+
import { basename as basename4, dirname as dirname3, join as join12 } from "node:path";
|
|
3685
3969
|
import { createHash as createHash4 } from "node:crypto";
|
|
3686
3970
|
var START = "<!-- FLOOM START -->";
|
|
3687
3971
|
var END = "<!-- FLOOM END -->";
|
|
@@ -3766,7 +4050,7 @@ async function writeManagedInstructionFile(input) {
|
|
|
3766
4050
|
else throw e;
|
|
3767
4051
|
}
|
|
3768
4052
|
const next = replaceManagedBlock(existing, input.blockBody);
|
|
3769
|
-
if (existed && !next.hadBlock && !input.apply
|
|
4053
|
+
if (existed && !next.hadBlock && !input.apply) {
|
|
3770
4054
|
log.warn(`Refusing to modify ${input.path} without an existing Floom managed block.`);
|
|
3771
4055
|
log.info("Re-run with --apply to append the managed block, or --path <file> for a dedicated file.");
|
|
3772
4056
|
process.exit(1);
|
|
@@ -3775,11 +4059,14 @@ async function writeManagedInstructionFile(input) {
|
|
|
3775
4059
|
let backupPath;
|
|
3776
4060
|
if (existed && existing) {
|
|
3777
4061
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3778
|
-
backupPath =
|
|
3779
|
-
await
|
|
4062
|
+
backupPath = join12(".floom", "backups", `${basename4(input.path)}.${stamp}.bak`);
|
|
4063
|
+
await assertPathIsNotSymlink(dirname3(backupPath), "instruction backup directory");
|
|
4064
|
+
await assertPathIsNotSymlink(backupPath, "instruction backup file");
|
|
4065
|
+
await mkdir7(dirname3(backupPath), { recursive: true, mode: 448 });
|
|
3780
4066
|
await writeFile5(backupPath, existing, { mode: 384 });
|
|
3781
4067
|
}
|
|
3782
|
-
await mkdir7(
|
|
4068
|
+
await mkdir7(dirname3(input.path), { recursive: true });
|
|
4069
|
+
await assertPathIsNotSymlink(input.path, "instruction file");
|
|
3783
4070
|
await writeFile5(input.path, next.content, "utf8");
|
|
3784
4071
|
return { changed: true, backupPath };
|
|
3785
4072
|
}
|
|
@@ -3932,7 +4219,7 @@ async function workspaceActiveCommand(opts = {}) {
|
|
|
3932
4219
|
|
|
3933
4220
|
// src/commands/sync.ts
|
|
3934
4221
|
import { mkdir as mkdir8, writeFile as writeFile6 } from "node:fs/promises";
|
|
3935
|
-
import { dirname as
|
|
4222
|
+
import { dirname as dirname4, join as join13 } from "node:path";
|
|
3936
4223
|
var ROUTER_SKILL = [
|
|
3937
4224
|
"# Floom Find Skills",
|
|
3938
4225
|
"",
|
|
@@ -3951,8 +4238,8 @@ var ROUTER_SKILL = [
|
|
|
3951
4238
|
async function installRouter(target) {
|
|
3952
4239
|
const install = resolveInstallDir({ target });
|
|
3953
4240
|
const routerDir = target === "kimi" ? "floom" : "floom-find-skills";
|
|
3954
|
-
const path =
|
|
3955
|
-
await mkdir8(
|
|
4241
|
+
const path = join13(install.dir, routerDir, "SKILL.md");
|
|
4242
|
+
await mkdir8(dirname4(path), { recursive: true });
|
|
3956
4243
|
await writeFile6(path, ROUTER_SKILL, "utf8");
|
|
3957
4244
|
return path;
|
|
3958
4245
|
}
|
|
@@ -4220,13 +4507,14 @@ async function folderRevokeCommand(workspace, folderId, memberId) {
|
|
|
4220
4507
|
}
|
|
4221
4508
|
|
|
4222
4509
|
// src/commands/mcp.ts
|
|
4223
|
-
import { mkdtemp, mkdir as mkdir9, readdir as
|
|
4224
|
-
import { join as
|
|
4225
|
-
import { tmpdir as
|
|
4510
|
+
import { mkdtemp, mkdir as mkdir9, readdir as readdir5, readFile as readFile10, rename as rename3, rm as rm3, writeFile as writeFile7 } from "node:fs/promises";
|
|
4511
|
+
import { join as join14 } from "node:path";
|
|
4512
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
4226
4513
|
import { z as z3 } from "zod";
|
|
4227
4514
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4228
4515
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4229
4516
|
var API_TIMEOUT_MS = 2e4;
|
|
4517
|
+
var MAX_ERROR_BODY_BYTES2 = 64 * 1024;
|
|
4230
4518
|
function semverGte2(a, b) {
|
|
4231
4519
|
const pa = a.split(".").map(Number);
|
|
4232
4520
|
const pb = b.split(".").map(Number);
|
|
@@ -4248,6 +4536,30 @@ async function fetchWithTimeout2(url, init = {}) {
|
|
|
4248
4536
|
clearTimeout(timer);
|
|
4249
4537
|
}
|
|
4250
4538
|
}
|
|
4539
|
+
async function readLimitedText2(res, limitBytes = MAX_ERROR_BODY_BYTES2) {
|
|
4540
|
+
if (!res.body) return res.text();
|
|
4541
|
+
const reader = res.body.getReader();
|
|
4542
|
+
const chunks = [];
|
|
4543
|
+
let total = 0;
|
|
4544
|
+
let truncated = false;
|
|
4545
|
+
while (true) {
|
|
4546
|
+
const { done, value } = await reader.read();
|
|
4547
|
+
if (done) break;
|
|
4548
|
+
const chunk = value ?? new Uint8Array();
|
|
4549
|
+
if (total + chunk.byteLength > limitBytes) {
|
|
4550
|
+
const remaining = Math.max(limitBytes - total, 0);
|
|
4551
|
+
if (remaining > 0) chunks.push(chunk.slice(0, remaining));
|
|
4552
|
+
truncated = true;
|
|
4553
|
+
await reader.cancel();
|
|
4554
|
+
break;
|
|
4555
|
+
}
|
|
4556
|
+
chunks.push(chunk);
|
|
4557
|
+
total += chunk.byteLength;
|
|
4558
|
+
}
|
|
4559
|
+
const text = new TextDecoder().decode(Buffer.concat(chunks));
|
|
4560
|
+
return truncated ? `${text}
|
|
4561
|
+
[truncated]` : text;
|
|
4562
|
+
}
|
|
4251
4563
|
async function resolveOptionalToken() {
|
|
4252
4564
|
const fromEnv = process.env.FLOOM_API_TOKEN?.trim();
|
|
4253
4565
|
if (fromEnv) return fromEnv;
|
|
@@ -4280,19 +4592,21 @@ async function apiRequest(token, path, query) {
|
|
|
4280
4592
|
lastError = new Error(`Unable to reach Floom API at ${base}: ${e.message}`);
|
|
4281
4593
|
continue;
|
|
4282
4594
|
}
|
|
4283
|
-
if (res.status >= 300 && res.status < 400) {
|
|
4284
|
-
|
|
4285
|
-
const redirectHost2 = location ? new URL(location, url).host : "unknown";
|
|
4286
|
-
lastError = new Error(`Floom API returned an unexpected redirect to ${redirectHost2}. Refusing to forward credentials.`);
|
|
4595
|
+
if (res.type === "opaqueredirect" || res.status >= 300 && res.status < 400) {
|
|
4596
|
+
lastError = new Error("Floom API returned an unexpected redirect. Refusing to forward credentials.");
|
|
4287
4597
|
break;
|
|
4288
4598
|
}
|
|
4289
|
-
const body = await res.text();
|
|
4599
|
+
const body = res.ok ? await res.text() : await readLimitedText2(res);
|
|
4290
4600
|
let json = null;
|
|
4291
4601
|
try {
|
|
4292
4602
|
json = body ? JSON.parse(body) : null;
|
|
4293
4603
|
} catch {
|
|
4294
4604
|
json = { raw: body };
|
|
4295
4605
|
}
|
|
4606
|
+
if (json?.error?.code) {
|
|
4607
|
+
lastError = new Error(json.error.message ?? String(json.error.code));
|
|
4608
|
+
if (res.ok || res.status !== 404) break;
|
|
4609
|
+
}
|
|
4296
4610
|
if (res.ok) return json;
|
|
4297
4611
|
lastError = new Error(json?.error?.message ?? `HTTP ${res.status}`);
|
|
4298
4612
|
if (res.status !== 404 || json?.error) break;
|
|
@@ -4311,14 +4625,16 @@ async function installViaApi(token, refText, target, options = {}) {
|
|
|
4311
4625
|
const bundle = await rawGet(dl.download.url);
|
|
4312
4626
|
if (!verifyBundleHash(bundle, dl.bundle_sha256)) throw new Error("Bundle hash mismatch");
|
|
4313
4627
|
const install = resolveInstallDir({ target });
|
|
4628
|
+
await assertPathIsNotSymlink(install.dir, "install directory");
|
|
4314
4629
|
await mkdir9(install.dir, { recursive: true });
|
|
4315
|
-
const dest =
|
|
4316
|
-
|
|
4630
|
+
const dest = join14(install.dir, parsed.slug);
|
|
4631
|
+
await assertPathIsNotSymlink(dest, "install destination");
|
|
4632
|
+
const exists = await readdir5(dest).then(() => true).catch(() => false);
|
|
4317
4633
|
if (exists && !options.force) {
|
|
4318
4634
|
throw new Error(`Folder already exists at ${dest}. Call install_skill with force=true to overwrite after reviewing local changes.`);
|
|
4319
4635
|
}
|
|
4320
4636
|
if (exists) await rm3(dest, { recursive: true, force: true });
|
|
4321
|
-
const temp = await mkdtemp(
|
|
4637
|
+
const temp = await mkdtemp(join14(tmpdir4(), `floom-mcp-${parsed.slug}-`));
|
|
4322
4638
|
try {
|
|
4323
4639
|
await extractBundle(bundle, temp);
|
|
4324
4640
|
await rename3(temp, dest);
|
|
@@ -4341,24 +4657,24 @@ async function installViaApi(token, refText, target, options = {}) {
|
|
|
4341
4657
|
return { path: dest, version: dl.version, ref: info.ref ?? ref, has_scripts: !!dl.has_scripts };
|
|
4342
4658
|
}
|
|
4343
4659
|
async function parseSkillBundle(bundle) {
|
|
4344
|
-
const tmp = await mkdtemp(
|
|
4660
|
+
const tmp = await mkdtemp(join14(tmpdir4(), "floom-mcp-read-"));
|
|
4345
4661
|
try {
|
|
4346
4662
|
await extractBundle(bundle, tmp);
|
|
4347
4663
|
const files = [];
|
|
4348
4664
|
const walk2 = async (dir, rel = "") => {
|
|
4349
|
-
const entries = await
|
|
4665
|
+
const entries = await readdir5(dir, { withFileTypes: true });
|
|
4350
4666
|
for (const entry of entries) {
|
|
4351
4667
|
const nextRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
4352
|
-
const full =
|
|
4668
|
+
const full = join14(dir, entry.name);
|
|
4353
4669
|
if (entry.isDirectory()) await walk2(full, nextRel);
|
|
4354
4670
|
else files.push(nextRel);
|
|
4355
4671
|
}
|
|
4356
4672
|
};
|
|
4357
4673
|
await walk2(tmp);
|
|
4358
4674
|
const skillMdPath = files.find((f) => f.toUpperCase() === "SKILL.MD");
|
|
4359
|
-
const skillMd = skillMdPath ? await readFile10(
|
|
4675
|
+
const skillMd = skillMdPath ? await readFile10(join14(tmp, skillMdPath), "utf8") : "";
|
|
4360
4676
|
const skillJsonPath = files.find((f) => f.toLowerCase() === "skill.json");
|
|
4361
|
-
const skillJson = skillJsonPath ? JSON.parse(await readFile10(
|
|
4677
|
+
const skillJson = skillJsonPath ? JSON.parse(await readFile10(join14(tmp, skillJsonPath), "utf8")) : null;
|
|
4362
4678
|
return { files, skill_md: skillMd, skill_json: skillJson };
|
|
4363
4679
|
} finally {
|
|
4364
4680
|
await rm3(tmp, { recursive: true, force: true });
|
|
@@ -4451,11 +4767,29 @@ async function mcpCommand() {
|
|
|
4451
4767
|
}
|
|
4452
4768
|
|
|
4453
4769
|
// src/commands/doctor.ts
|
|
4454
|
-
import { mkdtemp as mkdtemp2, readdir as
|
|
4455
|
-
import { tmpdir as
|
|
4456
|
-
import { join as
|
|
4770
|
+
import { mkdtemp as mkdtemp2, readdir as readdir6, rm as rm4 } from "node:fs/promises";
|
|
4771
|
+
import { tmpdir as tmpdir5 } from "node:os";
|
|
4772
|
+
import { join as join15 } from "node:path";
|
|
4773
|
+
import { fileURLToPath } from "node:url";
|
|
4457
4774
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
4458
4775
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
4776
|
+
|
|
4777
|
+
// src/lib/api-health.ts
|
|
4778
|
+
async function probeApiHealth(rawApiUrl) {
|
|
4779
|
+
const apiV1 = trustedApiUrlOrDefault(rawApiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
|
|
4780
|
+
const healthUrl = `${apiV1}/health`;
|
|
4781
|
+
try {
|
|
4782
|
+
const res = await fetch(healthUrl, { method: "GET", signal: AbortSignal.timeout(8e3) });
|
|
4783
|
+
if (!res.ok) return { name: "api_health", ok: false, detail: `GET ${healthUrl} \u2192 HTTP ${res.status}` };
|
|
4784
|
+
const body = await res.json();
|
|
4785
|
+
if (body.ok !== true) return { name: "api_health", ok: false, detail: `unexpected JSON from ${healthUrl}` };
|
|
4786
|
+
return { name: "api_health", ok: true, detail: healthUrl };
|
|
4787
|
+
} catch (e) {
|
|
4788
|
+
return { name: "api_health", ok: false, detail: `${healthUrl}: ${e.message}` };
|
|
4789
|
+
}
|
|
4790
|
+
}
|
|
4791
|
+
|
|
4792
|
+
// src/commands/doctor.ts
|
|
4459
4793
|
function textOf(result) {
|
|
4460
4794
|
return String(result?.content?.[0]?.text ?? "");
|
|
4461
4795
|
}
|
|
@@ -4519,11 +4853,13 @@ async function doctorCommand(opts = {}) {
|
|
|
4519
4853
|
authCheck2 = fail("auth", `saved login rejected by API: ${e.message}`);
|
|
4520
4854
|
}
|
|
4521
4855
|
}
|
|
4856
|
+
const resolvedApiUrl2 = process.env.FLOOM_API_URL ?? auth2?.apiUrl ?? DEFAULT_API_URL;
|
|
4522
4857
|
const checks2 = [
|
|
4523
4858
|
pass("cli_version", VERSION),
|
|
4524
4859
|
authCheck2,
|
|
4525
4860
|
process.env.FLOOM_API_URL ? apiUrlCheck(process.env.FLOOM_API_URL) : isLegacyApiUrl(rawAuth?.apiUrl) ? warn("auth_api_url", `legacy URL in ~/.floom/auth.json; using ${DEFAULT_API_URL}`) : apiUrlCheck(auth2?.apiUrl ?? DEFAULT_API_URL)
|
|
4526
4861
|
];
|
|
4862
|
+
checks2.push(await probeApiHealth(resolvedApiUrl2));
|
|
4527
4863
|
emitDoctor(checks2, opts.json);
|
|
4528
4864
|
if (checks2.some((check) => !check.ok)) process.exit(1);
|
|
4529
4865
|
return;
|
|
@@ -4531,18 +4867,20 @@ async function doctorCommand(opts = {}) {
|
|
|
4531
4867
|
const checks = [];
|
|
4532
4868
|
const auth = await readAuth();
|
|
4533
4869
|
const token = process.env.FLOOM_API_TOKEN?.trim() || auth?.token;
|
|
4870
|
+
const resolvedApiUrl = process.env.FLOOM_API_URL ?? auth?.apiUrl ?? DEFAULT_API_URL;
|
|
4871
|
+
checks.push(await probeApiHealth(resolvedApiUrl));
|
|
4534
4872
|
const authCheck = await validateCurrentToken(token);
|
|
4535
4873
|
const hasValidToken = authCheck.ok && authCheck.status !== "warn" && Boolean(token);
|
|
4536
4874
|
checks.push(authCheck);
|
|
4537
|
-
const cliPath =
|
|
4875
|
+
const cliPath = fileURLToPath(import.meta.url);
|
|
4538
4876
|
if (!cliPath) {
|
|
4539
|
-
checks.push(fail("fresh_agent_cli_path", "
|
|
4877
|
+
checks.push(fail("fresh_agent_cli_path", "current CLI module path is empty"));
|
|
4540
4878
|
emitDoctor(checks, opts.json);
|
|
4541
4879
|
process.exit(1);
|
|
4542
4880
|
}
|
|
4543
|
-
const tmpHome = await mkdtemp2(
|
|
4544
|
-
const tmpSkills = await mkdtemp2(
|
|
4545
|
-
const tmpProject = await mkdtemp2(
|
|
4881
|
+
const tmpHome = await mkdtemp2(join15(tmpdir5(), "floom-doctor-home-"));
|
|
4882
|
+
const tmpSkills = await mkdtemp2(join15(tmpdir5(), "floom-doctor-skills-"));
|
|
4883
|
+
const tmpProject = await mkdtemp2(join15(tmpdir5(), "floom-doctor-project-"));
|
|
4546
4884
|
const transport = new StdioClientTransport({
|
|
4547
4885
|
command: process.execPath,
|
|
4548
4886
|
args: [cliPath, "mcp"],
|
|
@@ -4582,7 +4920,7 @@ async function doctorCommand(opts = {}) {
|
|
|
4582
4920
|
const skill = await client.callTool({ name: "get_skill", arguments: { ref: opts.ref } });
|
|
4583
4921
|
checks.push(/SKILL\.md/.test(textOf(skill)) ? pass("mcp_get_skill", opts.ref) : fail("mcp_get_skill", "bundle did not include SKILL.md"));
|
|
4584
4922
|
const installed = await client.callTool({ name: "install_skill", arguments: { ref: opts.ref, target: opts.target ?? "codex" } });
|
|
4585
|
-
const entries = await
|
|
4923
|
+
const entries = await readdir6(tmpSkills);
|
|
4586
4924
|
checks.push(entries.length > 0 ? pass("mcp_install_skill", textOf(installed)) : fail("mcp_install_skill", "target directory is empty"));
|
|
4587
4925
|
} else {
|
|
4588
4926
|
checks.push(warn("mcp_public_skill", "skipped; pass --ref <public-skill-ref> to verify get_skill/install_skill against a known public skill"));
|
|
@@ -4621,9 +4959,9 @@ program.command("logout").description("Log out and revoke local token.").action(
|
|
|
4621
4959
|
program.command("whoami").description("Show the logged-in user.").action(whoamiCommand);
|
|
4622
4960
|
program.command("init").description("Scaffold a new skill in the current directory.").action(initCommand);
|
|
4623
4961
|
program.command("validate").description("Validate the skill in the current directory.").option("--json", "Emit machine-readable JSON").action((opts) => validateCommand(opts));
|
|
4624
|
-
program.command("publish").description("Publish the skill in the current directory.").option("--dry-run", "Validate and pack locally without uploading.").option("--workspace <slug>", "Publish into a shared workspace slug (default: personal)").option("--library <slug>", "Legacy alias for --workspace").addHelpText("after", "\nExamples:\n $ floom publish\n $ floom publish --workspace team-workspace").action((opts) => publishCommand(opts));
|
|
4625
|
-
program.command("push").description("
|
|
4626
|
-
program.command("install <ref>").description("Install a skill (default: .agents/skills/<slug>/).").option("--for <target>", "Tool preset: claude | codex | cursor | gemini | opencode | kimi | all").option("--to <path>", "
|
|
4962
|
+
program.command("publish").description("Publish the skill in the current directory.").option("--dry-run", "Validate and pack locally without uploading.").option("--workspace <slug>", "Publish into a shared workspace slug (default: personal)").option("--library <slug>", "Legacy alias for --workspace").option("--folder <uuid>", "Publish into a workspace folder id").addHelpText("after", "\nExamples:\n $ floom publish\n $ floom publish --workspace team-workspace\n $ floom publish --workspace team-workspace --folder 00000000-0000-0000-0000-000000000000").action((opts) => publishCommand(opts));
|
|
4963
|
+
program.command("push [dir]").description("Publish one skill folder or every immediate child skill folder.").option("--dry-run", "Validate and pack locally without uploading.").option("--workspace <slug>", "Publish into a shared workspace slug").option("--library <slug>", "Legacy alias for --workspace").option("--folder <uuid>", "Publish into a workspace folder id").option("--concurrency <n>", "Bulk push concurrency, 1-16", "6").addHelpText("after", "\nExamples:\n $ floom push\n $ floom push ./skills --workspace team-workspace --concurrency 4\n $ floom push ./skills --workspace team-workspace --folder 00000000-0000-0000-0000-000000000000").action((dir, opts) => pushCommand(dir ?? ".", opts));
|
|
4964
|
+
program.command("install <ref>").description("Install a skill (default: .agents/skills/<slug>/).").option("--for <target>", "Tool preset: claude | codex | cursor | gemini | opencode | kimi | all").option("--to <path>", "Parent directory; installs to <path>/<skill-slug>/ (not the skill folder itself)").option("--global", "Install to user-level folder instead of project-local").option("--force", "Overwrite existing folder").addHelpText("after", "\nExamples:\n $ floom install @alice/research-brief\n $ floom install @alice/research-brief --for codex\n $ floom install @alice/research-brief --to .agents/skills\n\nNote: --to is the parent folder. The skill lands in .agents/skills/research-brief/, not directly in .agents/skills/.").action((ref, opts) => installCommand(ref, opts));
|
|
4627
4965
|
program.command("installed").description("List installed skills in this project.").option("--json").action(installedCommand);
|
|
4628
4966
|
program.command("outdated").description("Show installed skills with newer versions available.").action(outdatedCommand);
|
|
4629
4967
|
program.command("update [ref]").description("Update installed skills to latest.").option("--force", "Overwrite local edits").action((ref, opts) => updateCommand(ref, opts));
|
|
@@ -4636,6 +4974,8 @@ program.command("pinned").alias("pins").description("List workspace skills pinne
|
|
|
4636
4974
|
program.command("unpin <ref>").description("Unpin a workspace skill for local pull").option("--workspace <slug>", "Workspace slug").option("--target <target>", "claude | codex | cursor | kimi | opencode", "codex").action((ref, opts) => unpinCommand(ref, opts));
|
|
4637
4975
|
program.command("share <ref> <email>").description("Invite someone to a skill by email.").option("--role <role>", "viewer (default) or editor").action((ref, email, opts) => shareCommand(ref, email, opts));
|
|
4638
4976
|
program.command("unshare <ref> <email>").description("Revoke someone's access.").action((ref, email) => unshareCommand(ref, email));
|
|
4977
|
+
var linkCmd = program.command("link").description("Create opaque share links for unlisted/public skills");
|
|
4978
|
+
linkCmd.command("create <ref>").description("Create a share link URL for a skill.").option("--name <name>", "Optional link label").option("--role <role>", "viewer (default) or editor").action((ref, opts) => linkCreateCommand(ref, opts));
|
|
4639
4979
|
var configCmd = program.command("config").description("Manage local Floom configuration");
|
|
4640
4980
|
configCmd.command("default-workspace [slug]").description("Show or set the default workspace").option("--scope <scope>", "global | local", "local").action((slug, opts) => defaultWorkspaceCommand(slug, opts));
|
|
4641
4981
|
function addWorkspaceCommands(cmd) {
|
|
@@ -4694,4 +5034,3 @@ bcryptjs/dist/bcrypt.js:
|
|
|
4694
5034
|
* see: https://github.com/dcodeIO/bcrypt.js for details
|
|
4695
5035
|
*)
|
|
4696
5036
|
*/
|
|
4697
|
-
//# sourceMappingURL=index.js.map
|