@floomhq/skills 0.2.13 → 0.2.17
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 +444 -108
- 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.17";
|
|
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) {
|
|
@@ -3070,34 +3221,126 @@ async function publishCommand(opts = {}) {
|
|
|
3070
3221
|
log.blank();
|
|
3071
3222
|
log.ok(`Published ${complete.ref}`);
|
|
3072
3223
|
log.blank();
|
|
3073
|
-
log.info("Manage:");
|
|
3074
3224
|
const displayApiUrl = trustedApiUrlOrDefault(process.env.FLOOM_API_URL ?? auth?.apiUrl ?? DEFAULT_API_URL);
|
|
3075
|
-
|
|
3076
|
-
const
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3225
|
+
const appUrl = displayApiUrl.replace(/\/api\/v1\/?$/, "");
|
|
3226
|
+
const visibility = complete.visibility ?? "private";
|
|
3227
|
+
const views = buildPublishViewLines({
|
|
3228
|
+
visibility,
|
|
3229
|
+
appUrl,
|
|
3230
|
+
handle,
|
|
3231
|
+
slug: manifest.name,
|
|
3232
|
+
refRoot,
|
|
3233
|
+
shareUrl: complete.share_url
|
|
3234
|
+
});
|
|
3235
|
+
log.info(views.heading);
|
|
3236
|
+
log.kv("", views.primaryUrl);
|
|
3237
|
+
if (views.secondaryHeading && views.secondaryUrl) {
|
|
3238
|
+
log.info(views.secondaryHeading);
|
|
3239
|
+
log.kv("", views.secondaryUrl);
|
|
3240
|
+
}
|
|
3241
|
+
if (views.note) {
|
|
3242
|
+
log.info(views.note);
|
|
3243
|
+
}
|
|
3244
|
+
if (views.shareUrl) {
|
|
3245
|
+
log.info("Share link:");
|
|
3246
|
+
log.kv("", views.shareUrl);
|
|
3247
|
+
} else if (visibility === "unlisted") {
|
|
3248
|
+
log.info(`Create a share link: floom link create ${refRoot}/${manifest.name}`);
|
|
3083
3249
|
}
|
|
3084
3250
|
log.info("Install:");
|
|
3085
3251
|
log.kv("", complete.install_command);
|
|
3086
3252
|
}
|
|
3087
3253
|
|
|
3254
|
+
// src/commands/push.ts
|
|
3255
|
+
import { readdir as readdir2, stat as stat3 } from "node:fs/promises";
|
|
3256
|
+
import { basename as basename3, join as join7, resolve as resolve2 } from "node:path";
|
|
3257
|
+
function parsePushConcurrency(value) {
|
|
3258
|
+
const raw = value ?? 6;
|
|
3259
|
+
const parsed = typeof raw === "number" ? raw : Number.parseInt(raw, 10);
|
|
3260
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 16) {
|
|
3261
|
+
throw new Error("--concurrency must be an integer from 1 to 16.");
|
|
3262
|
+
}
|
|
3263
|
+
return parsed;
|
|
3264
|
+
}
|
|
3265
|
+
async function hasSkillFiles(dir) {
|
|
3266
|
+
try {
|
|
3267
|
+
const [skillMd, manifest] = await Promise.all([
|
|
3268
|
+
stat3(join7(dir, "SKILL.md")),
|
|
3269
|
+
stat3(join7(dir, "skill.json"))
|
|
3270
|
+
]);
|
|
3271
|
+
return skillMd.isFile() && manifest.isFile();
|
|
3272
|
+
} catch {
|
|
3273
|
+
return false;
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
async function findImmediateSkillDirs(root) {
|
|
3277
|
+
const entries = await readdir2(root, { withFileTypes: true });
|
|
3278
|
+
const dirs = entries.filter((entry) => entry.isDirectory()).map((entry) => join7(root, entry.name)).sort();
|
|
3279
|
+
const checks = await Promise.all(dirs.map(async (dir) => await hasSkillFiles(dir) ? dir : null));
|
|
3280
|
+
return checks.filter((dir) => Boolean(dir));
|
|
3281
|
+
}
|
|
3282
|
+
async function runBounded(items, concurrency, worker) {
|
|
3283
|
+
let next = 0;
|
|
3284
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
3285
|
+
while (next < items.length) {
|
|
3286
|
+
const item = items[next];
|
|
3287
|
+
next += 1;
|
|
3288
|
+
await worker(item);
|
|
3289
|
+
}
|
|
3290
|
+
});
|
|
3291
|
+
await Promise.all(workers);
|
|
3292
|
+
}
|
|
3293
|
+
async function pushCommand(dir = ".", options = {}, deps = {}) {
|
|
3294
|
+
const root = resolve2(dir);
|
|
3295
|
+
const rootStat = await stat3(root).catch(() => null);
|
|
3296
|
+
if (!rootStat?.isDirectory()) {
|
|
3297
|
+
throw new Error(`Directory not found: ${dir}`);
|
|
3298
|
+
}
|
|
3299
|
+
const publish = deps.publish ?? publishCommand;
|
|
3300
|
+
if (await hasSkillFiles(root)) {
|
|
3301
|
+
await publish({ ...options, dir: root });
|
|
3302
|
+
return;
|
|
3303
|
+
}
|
|
3304
|
+
const skillDirs = await findImmediateSkillDirs(root);
|
|
3305
|
+
if (skillDirs.length === 0) {
|
|
3306
|
+
throw new Error("SKILL.md and skill.json are required, either in this directory or immediate child directories.");
|
|
3307
|
+
}
|
|
3308
|
+
const concurrency = parsePushConcurrency(options.concurrency);
|
|
3309
|
+
const startedAt = Date.now();
|
|
3310
|
+
const errors = [];
|
|
3311
|
+
let pushed = 0;
|
|
3312
|
+
await runBounded(skillDirs, concurrency, async (skillDir) => {
|
|
3313
|
+
const slug = basename3(skillDir);
|
|
3314
|
+
try {
|
|
3315
|
+
await publish({ ...options, dir: skillDir });
|
|
3316
|
+
pushed += 1;
|
|
3317
|
+
log.info(`Pushed ${slug}`);
|
|
3318
|
+
} catch (error) {
|
|
3319
|
+
errors.push({ slug, message: error.message });
|
|
3320
|
+
}
|
|
3321
|
+
});
|
|
3322
|
+
const elapsed = ((Date.now() - startedAt) / 1e3).toFixed(1).replace(/\.0$/, "");
|
|
3323
|
+
log.info(`Pushed ${pushed}/${skillDirs.length} skills in ${elapsed}s.`);
|
|
3324
|
+
if (errors.length > 0) {
|
|
3325
|
+
log.err("Push errors:");
|
|
3326
|
+
for (const error of errors) log.err(`- ${error.slug}: ${error.message}`);
|
|
3327
|
+
process.exitCode = 1;
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3088
3331
|
// src/commands/install.ts
|
|
3089
|
-
import { mkdir as mkdir4, readdir as
|
|
3090
|
-
import { join as
|
|
3091
|
-
import { tmpdir } from "node:os";
|
|
3332
|
+
import { mkdir as mkdir4, readdir as readdir4, rm, rename } from "node:fs/promises";
|
|
3333
|
+
import { join as join9 } from "node:path";
|
|
3334
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
3092
3335
|
|
|
3093
3336
|
// src/lib/floom-lock.ts
|
|
3094
|
-
import { readFile as readFile7, writeFile as writeFile3, mkdir as mkdir3, stat as
|
|
3095
|
-
import { join as
|
|
3337
|
+
import { readFile as readFile7, writeFile as writeFile3, mkdir as mkdir3, stat as stat4, readdir as readdir3 } from "node:fs/promises";
|
|
3338
|
+
import { join as join8, relative as relative3, sep as sep2, posix as posix2 } from "node:path";
|
|
3096
3339
|
import { createHash as createHash3 } from "node:crypto";
|
|
3097
3340
|
var EMPTY = { schema_version: "0.1", skills: {} };
|
|
3098
3341
|
async function readLock(projectDir) {
|
|
3099
3342
|
try {
|
|
3100
|
-
const raw = await readFile7(
|
|
3343
|
+
const raw = await readFile7(join8(projectDir, "floom.lock"), "utf8");
|
|
3101
3344
|
const parsed = JSON.parse(raw);
|
|
3102
3345
|
if (parsed.schema_version === "0.1") return parsed;
|
|
3103
3346
|
if (parsed.schema_version === "0.2") return parsed;
|
|
@@ -3112,7 +3355,7 @@ async function readLock(projectDir) {
|
|
|
3112
3355
|
}
|
|
3113
3356
|
async function writeLock(projectDir, lock) {
|
|
3114
3357
|
await mkdir3(projectDir, { recursive: true });
|
|
3115
|
-
await writeFile3(
|
|
3358
|
+
await writeFile3(join8(projectDir, "floom.lock"), JSON.stringify(lock, null, 2) + "\n", "utf8");
|
|
3116
3359
|
}
|
|
3117
3360
|
function setLockEntry(lock, ref, entry) {
|
|
3118
3361
|
return { ...lock, skills: { ...lock.skills, [ref]: entry } };
|
|
@@ -3142,12 +3385,12 @@ async function hashInstalledFolder(folderAbs) {
|
|
|
3142
3385
|
async function walk2(dir) {
|
|
3143
3386
|
let entries;
|
|
3144
3387
|
try {
|
|
3145
|
-
entries = await
|
|
3388
|
+
entries = await readdir3(dir, { withFileTypes: true });
|
|
3146
3389
|
} catch {
|
|
3147
3390
|
return;
|
|
3148
3391
|
}
|
|
3149
3392
|
for (const entry of entries) {
|
|
3150
|
-
const abs =
|
|
3393
|
+
const abs = join8(dir, entry.name);
|
|
3151
3394
|
if (entry.isSymbolicLink()) continue;
|
|
3152
3395
|
if (entry.isDirectory()) {
|
|
3153
3396
|
await walk2(abs);
|
|
@@ -3155,7 +3398,7 @@ async function hashInstalledFolder(folderAbs) {
|
|
|
3155
3398
|
}
|
|
3156
3399
|
if (!entry.isFile()) continue;
|
|
3157
3400
|
const buf = await readFile7(abs);
|
|
3158
|
-
const rel =
|
|
3401
|
+
const rel = relative3(folderAbs, abs).split(sep2).join(posix2.sep);
|
|
3159
3402
|
out.push({
|
|
3160
3403
|
relPath: rel,
|
|
3161
3404
|
sha256: createHash3("sha256").update(buf).digest("hex"),
|
|
@@ -3164,7 +3407,7 @@ async function hashInstalledFolder(folderAbs) {
|
|
|
3164
3407
|
}
|
|
3165
3408
|
}
|
|
3166
3409
|
try {
|
|
3167
|
-
await
|
|
3410
|
+
await stat4(folderAbs);
|
|
3168
3411
|
} catch {
|
|
3169
3412
|
return out;
|
|
3170
3413
|
}
|
|
@@ -3173,6 +3416,20 @@ async function hashInstalledFolder(folderAbs) {
|
|
|
3173
3416
|
return out;
|
|
3174
3417
|
}
|
|
3175
3418
|
|
|
3419
|
+
// src/lib/filesystem-safety.ts
|
|
3420
|
+
import { lstat as lstat2 } from "node:fs/promises";
|
|
3421
|
+
async function assertPathIsNotSymlink(path, label = "path") {
|
|
3422
|
+
try {
|
|
3423
|
+
const stats = await lstat2(path);
|
|
3424
|
+
if (stats.isSymbolicLink()) {
|
|
3425
|
+
throw new Error(`${label} must not be a symbolic link: ${path}`);
|
|
3426
|
+
}
|
|
3427
|
+
} catch (e) {
|
|
3428
|
+
if (e.code === "ENOENT") return;
|
|
3429
|
+
throw e;
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
|
|
3176
3433
|
// src/commands/install.ts
|
|
3177
3434
|
var CLI_VERSION = VERSION;
|
|
3178
3435
|
function semverGte(a, b) {
|
|
@@ -3226,13 +3483,16 @@ async function installCommand(refStr, opts = {}) {
|
|
|
3226
3483
|
global: opts.global
|
|
3227
3484
|
});
|
|
3228
3485
|
const projectDir = opts.global ? process.cwd() : process.cwd();
|
|
3229
|
-
const
|
|
3486
|
+
const parentDir = opts.to ? normalizeInstallParentDir(target.dir, ref.slug) : target.dir;
|
|
3487
|
+
const destFolder = join9(parentDir, ref.slug);
|
|
3488
|
+
await assertPathIsNotSymlink(parentDir, "install parent directory");
|
|
3489
|
+
await assertPathIsNotSymlink(destFolder, "install destination");
|
|
3230
3490
|
const lock = await readLock(projectDir);
|
|
3231
3491
|
const refKey = formatSkillRef({ owner: ref.owner, slug: ref.slug });
|
|
3232
3492
|
const existing = lock.skills[refKey];
|
|
3233
3493
|
let folderExists = false;
|
|
3234
3494
|
try {
|
|
3235
|
-
const entries = await
|
|
3495
|
+
const entries = await readdir4(destFolder);
|
|
3236
3496
|
folderExists = entries.length > 0;
|
|
3237
3497
|
} catch {
|
|
3238
3498
|
}
|
|
@@ -3262,7 +3522,7 @@ async function installCommand(refStr, opts = {}) {
|
|
|
3262
3522
|
}
|
|
3263
3523
|
log.ok(`Bundle verified (${(buf.length / 1024).toFixed(1)} KB).`);
|
|
3264
3524
|
const tmp = await import("node:fs/promises").then(
|
|
3265
|
-
(m) => m.mkdtemp(
|
|
3525
|
+
(m) => m.mkdtemp(join9(tmpdir2(), `floom-install-${ref.slug}-`))
|
|
3266
3526
|
);
|
|
3267
3527
|
try {
|
|
3268
3528
|
await extractBundle(buf, tmp);
|
|
@@ -3365,8 +3625,8 @@ async function outdatedCommand() {
|
|
|
3365
3625
|
|
|
3366
3626
|
// src/commands/update.ts
|
|
3367
3627
|
import { rm as rm2, rename as rename2, mkdir as mkdir5 } from "node:fs/promises";
|
|
3368
|
-
import { tmpdir as
|
|
3369
|
-
import { join as
|
|
3628
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
3629
|
+
import { join as join10, resolve as resolve4 } from "node:path";
|
|
3370
3630
|
function cmpSemver2(a, b) {
|
|
3371
3631
|
const pa = a.split(".").map((p) => parseInt(p, 10));
|
|
3372
3632
|
const pb = b.split(".").map((p) => parseInt(p, 10));
|
|
@@ -3405,7 +3665,8 @@ async function updateCommand(refStr, opts = {}) {
|
|
|
3405
3665
|
log.step(`${ref} is already at latest (${entry.version}).`);
|
|
3406
3666
|
continue;
|
|
3407
3667
|
}
|
|
3408
|
-
const installDir =
|
|
3668
|
+
const installDir = resolve4(projectDir, entry.path);
|
|
3669
|
+
await assertPathIsNotSymlink(installDir, "installed skill directory");
|
|
3409
3670
|
if (!opts.force) {
|
|
3410
3671
|
const current = await hashInstalledFolder(installDir);
|
|
3411
3672
|
log.step(`Updating ${ref}: ${entry.version} -> ${info.latest_version}`);
|
|
@@ -3418,7 +3679,7 @@ async function updateCommand(refStr, opts = {}) {
|
|
|
3418
3679
|
continue;
|
|
3419
3680
|
}
|
|
3420
3681
|
const tmp = await import("node:fs/promises").then(
|
|
3421
|
-
(m) => m.mkdtemp(
|
|
3682
|
+
(m) => m.mkdtemp(join10(tmpdir3(), `floom-update-${slug}-`))
|
|
3422
3683
|
);
|
|
3423
3684
|
try {
|
|
3424
3685
|
await extractBundle(buf, tmp);
|
|
@@ -3545,6 +3806,26 @@ async function unshareCommand(refStr, email) {
|
|
|
3545
3806
|
log.ok(`Removed ${email}'s access to ${refStr}.`);
|
|
3546
3807
|
}
|
|
3547
3808
|
|
|
3809
|
+
// src/commands/link.ts
|
|
3810
|
+
async function linkCreateCommand(refStr, opts = {}) {
|
|
3811
|
+
const ref = parseSkillRef(refStr);
|
|
3812
|
+
if (!ref) {
|
|
3813
|
+
log.err(`Invalid skill ref: ${refStr}`);
|
|
3814
|
+
process.exit(1);
|
|
3815
|
+
}
|
|
3816
|
+
const role = opts.role ?? "viewer";
|
|
3817
|
+
const body = { role };
|
|
3818
|
+
if (opts.name) body.name = opts.name;
|
|
3819
|
+
const r = await api(`/skills/${ref.owner}/${ref.slug}/links`, {
|
|
3820
|
+
method: "POST",
|
|
3821
|
+
authRequired: true,
|
|
3822
|
+
body
|
|
3823
|
+
});
|
|
3824
|
+
log.ok(`Share link created for ${refStr}.`);
|
|
3825
|
+
log.kv("url", r.link.url);
|
|
3826
|
+
log.kv("role", r.link.role);
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3548
3829
|
// src/commands/library.ts
|
|
3549
3830
|
function isValidEmail2(email) {
|
|
3550
3831
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
@@ -3614,7 +3895,7 @@ async function workspaceAcceptCommand(tokenOrUrl) {
|
|
|
3614
3895
|
// src/lib/config-file.ts
|
|
3615
3896
|
import { mkdir as mkdir6, readFile as readFile8, writeFile as writeFile4 } from "node:fs/promises";
|
|
3616
3897
|
import { homedir as homedir3 } from "node:os";
|
|
3617
|
-
import { dirname, join as
|
|
3898
|
+
import { dirname as dirname2, join as join11 } from "node:path";
|
|
3618
3899
|
import { z as z2 } from "zod";
|
|
3619
3900
|
var FLOOM_CONFIG_VERSION = "0.1";
|
|
3620
3901
|
var TARGETS = ["claude", "codex", "cursor", "kimi", "opencode"];
|
|
@@ -3628,8 +3909,8 @@ var configSchema = z2.object({
|
|
|
3628
3909
|
targets: z2.record(z2.enum(TARGETS), targetConfigSchema).default({})
|
|
3629
3910
|
});
|
|
3630
3911
|
function configPath(scope, cwd = process.cwd()) {
|
|
3631
|
-
if (scope === "global") return
|
|
3632
|
-
return
|
|
3912
|
+
if (scope === "global") return join11(homedir3(), ".floom", "config.json");
|
|
3913
|
+
return join11(cwd, ".floom", "config.json");
|
|
3633
3914
|
}
|
|
3634
3915
|
async function readFloomConfig(scope, cwd = process.cwd()) {
|
|
3635
3916
|
try {
|
|
@@ -3643,7 +3924,7 @@ async function readFloomConfig(scope, cwd = process.cwd()) {
|
|
|
3643
3924
|
async function writeFloomConfig(scope, config, cwd = process.cwd()) {
|
|
3644
3925
|
const path = configPath(scope, cwd);
|
|
3645
3926
|
const parsed = configSchema.parse(config);
|
|
3646
|
-
await mkdir6(
|
|
3927
|
+
await mkdir6(dirname2(path), { recursive: true, mode: 448 });
|
|
3647
3928
|
await writeFile4(path, JSON.stringify(parsed, null, 2) + "\n", { mode: 384 });
|
|
3648
3929
|
}
|
|
3649
3930
|
function normalizeScope(value) {
|
|
@@ -3681,7 +3962,7 @@ async function setWorkspaceActive(input) {
|
|
|
3681
3962
|
|
|
3682
3963
|
// src/commands/instruction.ts
|
|
3683
3964
|
import { mkdir as mkdir7, readFile as readFile9, writeFile as writeFile5 } from "node:fs/promises";
|
|
3684
|
-
import { basename as
|
|
3965
|
+
import { basename as basename4, dirname as dirname3, join as join12 } from "node:path";
|
|
3685
3966
|
import { createHash as createHash4 } from "node:crypto";
|
|
3686
3967
|
var START = "<!-- FLOOM START -->";
|
|
3687
3968
|
var END = "<!-- FLOOM END -->";
|
|
@@ -3766,7 +4047,7 @@ async function writeManagedInstructionFile(input) {
|
|
|
3766
4047
|
else throw e;
|
|
3767
4048
|
}
|
|
3768
4049
|
const next = replaceManagedBlock(existing, input.blockBody);
|
|
3769
|
-
if (existed && !next.hadBlock && !input.apply
|
|
4050
|
+
if (existed && !next.hadBlock && !input.apply) {
|
|
3770
4051
|
log.warn(`Refusing to modify ${input.path} without an existing Floom managed block.`);
|
|
3771
4052
|
log.info("Re-run with --apply to append the managed block, or --path <file> for a dedicated file.");
|
|
3772
4053
|
process.exit(1);
|
|
@@ -3775,11 +4056,14 @@ async function writeManagedInstructionFile(input) {
|
|
|
3775
4056
|
let backupPath;
|
|
3776
4057
|
if (existed && existing) {
|
|
3777
4058
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3778
|
-
backupPath =
|
|
3779
|
-
await
|
|
4059
|
+
backupPath = join12(".floom", "backups", `${basename4(input.path)}.${stamp}.bak`);
|
|
4060
|
+
await assertPathIsNotSymlink(dirname3(backupPath), "instruction backup directory");
|
|
4061
|
+
await assertPathIsNotSymlink(backupPath, "instruction backup file");
|
|
4062
|
+
await mkdir7(dirname3(backupPath), { recursive: true, mode: 448 });
|
|
3780
4063
|
await writeFile5(backupPath, existing, { mode: 384 });
|
|
3781
4064
|
}
|
|
3782
|
-
await mkdir7(
|
|
4065
|
+
await mkdir7(dirname3(input.path), { recursive: true });
|
|
4066
|
+
await assertPathIsNotSymlink(input.path, "instruction file");
|
|
3783
4067
|
await writeFile5(input.path, next.content, "utf8");
|
|
3784
4068
|
return { changed: true, backupPath };
|
|
3785
4069
|
}
|
|
@@ -3932,7 +4216,7 @@ async function workspaceActiveCommand(opts = {}) {
|
|
|
3932
4216
|
|
|
3933
4217
|
// src/commands/sync.ts
|
|
3934
4218
|
import { mkdir as mkdir8, writeFile as writeFile6 } from "node:fs/promises";
|
|
3935
|
-
import { dirname as
|
|
4219
|
+
import { dirname as dirname4, join as join13 } from "node:path";
|
|
3936
4220
|
var ROUTER_SKILL = [
|
|
3937
4221
|
"# Floom Find Skills",
|
|
3938
4222
|
"",
|
|
@@ -3951,8 +4235,8 @@ var ROUTER_SKILL = [
|
|
|
3951
4235
|
async function installRouter(target) {
|
|
3952
4236
|
const install = resolveInstallDir({ target });
|
|
3953
4237
|
const routerDir = target === "kimi" ? "floom" : "floom-find-skills";
|
|
3954
|
-
const path =
|
|
3955
|
-
await mkdir8(
|
|
4238
|
+
const path = join13(install.dir, routerDir, "SKILL.md");
|
|
4239
|
+
await mkdir8(dirname4(path), { recursive: true });
|
|
3956
4240
|
await writeFile6(path, ROUTER_SKILL, "utf8");
|
|
3957
4241
|
return path;
|
|
3958
4242
|
}
|
|
@@ -4220,13 +4504,14 @@ async function folderRevokeCommand(workspace, folderId, memberId) {
|
|
|
4220
4504
|
}
|
|
4221
4505
|
|
|
4222
4506
|
// src/commands/mcp.ts
|
|
4223
|
-
import { mkdtemp, mkdir as mkdir9, readdir as
|
|
4224
|
-
import { join as
|
|
4225
|
-
import { tmpdir as
|
|
4507
|
+
import { mkdtemp, mkdir as mkdir9, readdir as readdir5, readFile as readFile10, rename as rename3, rm as rm3, writeFile as writeFile7 } from "node:fs/promises";
|
|
4508
|
+
import { join as join14 } from "node:path";
|
|
4509
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
4226
4510
|
import { z as z3 } from "zod";
|
|
4227
4511
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4228
4512
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4229
4513
|
var API_TIMEOUT_MS = 2e4;
|
|
4514
|
+
var MAX_ERROR_BODY_BYTES2 = 64 * 1024;
|
|
4230
4515
|
function semverGte2(a, b) {
|
|
4231
4516
|
const pa = a.split(".").map(Number);
|
|
4232
4517
|
const pb = b.split(".").map(Number);
|
|
@@ -4248,6 +4533,30 @@ async function fetchWithTimeout2(url, init = {}) {
|
|
|
4248
4533
|
clearTimeout(timer);
|
|
4249
4534
|
}
|
|
4250
4535
|
}
|
|
4536
|
+
async function readLimitedText2(res, limitBytes = MAX_ERROR_BODY_BYTES2) {
|
|
4537
|
+
if (!res.body) return res.text();
|
|
4538
|
+
const reader = res.body.getReader();
|
|
4539
|
+
const chunks = [];
|
|
4540
|
+
let total = 0;
|
|
4541
|
+
let truncated = false;
|
|
4542
|
+
while (true) {
|
|
4543
|
+
const { done, value } = await reader.read();
|
|
4544
|
+
if (done) break;
|
|
4545
|
+
const chunk = value ?? new Uint8Array();
|
|
4546
|
+
if (total + chunk.byteLength > limitBytes) {
|
|
4547
|
+
const remaining = Math.max(limitBytes - total, 0);
|
|
4548
|
+
if (remaining > 0) chunks.push(chunk.slice(0, remaining));
|
|
4549
|
+
truncated = true;
|
|
4550
|
+
await reader.cancel();
|
|
4551
|
+
break;
|
|
4552
|
+
}
|
|
4553
|
+
chunks.push(chunk);
|
|
4554
|
+
total += chunk.byteLength;
|
|
4555
|
+
}
|
|
4556
|
+
const text = new TextDecoder().decode(Buffer.concat(chunks));
|
|
4557
|
+
return truncated ? `${text}
|
|
4558
|
+
[truncated]` : text;
|
|
4559
|
+
}
|
|
4251
4560
|
async function resolveOptionalToken() {
|
|
4252
4561
|
const fromEnv = process.env.FLOOM_API_TOKEN?.trim();
|
|
4253
4562
|
if (fromEnv) return fromEnv;
|
|
@@ -4280,19 +4589,21 @@ async function apiRequest(token, path, query) {
|
|
|
4280
4589
|
lastError = new Error(`Unable to reach Floom API at ${base}: ${e.message}`);
|
|
4281
4590
|
continue;
|
|
4282
4591
|
}
|
|
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.`);
|
|
4592
|
+
if (res.type === "opaqueredirect" || res.status >= 300 && res.status < 400) {
|
|
4593
|
+
lastError = new Error("Floom API returned an unexpected redirect. Refusing to forward credentials.");
|
|
4287
4594
|
break;
|
|
4288
4595
|
}
|
|
4289
|
-
const body = await res.text();
|
|
4596
|
+
const body = res.ok ? await res.text() : await readLimitedText2(res);
|
|
4290
4597
|
let json = null;
|
|
4291
4598
|
try {
|
|
4292
4599
|
json = body ? JSON.parse(body) : null;
|
|
4293
4600
|
} catch {
|
|
4294
4601
|
json = { raw: body };
|
|
4295
4602
|
}
|
|
4603
|
+
if (json?.error?.code) {
|
|
4604
|
+
lastError = new Error(json.error.message ?? String(json.error.code));
|
|
4605
|
+
if (res.ok || res.status !== 404) break;
|
|
4606
|
+
}
|
|
4296
4607
|
if (res.ok) return json;
|
|
4297
4608
|
lastError = new Error(json?.error?.message ?? `HTTP ${res.status}`);
|
|
4298
4609
|
if (res.status !== 404 || json?.error) break;
|
|
@@ -4311,14 +4622,16 @@ async function installViaApi(token, refText, target, options = {}) {
|
|
|
4311
4622
|
const bundle = await rawGet(dl.download.url);
|
|
4312
4623
|
if (!verifyBundleHash(bundle, dl.bundle_sha256)) throw new Error("Bundle hash mismatch");
|
|
4313
4624
|
const install = resolveInstallDir({ target });
|
|
4625
|
+
await assertPathIsNotSymlink(install.dir, "install directory");
|
|
4314
4626
|
await mkdir9(install.dir, { recursive: true });
|
|
4315
|
-
const dest =
|
|
4316
|
-
|
|
4627
|
+
const dest = join14(install.dir, parsed.slug);
|
|
4628
|
+
await assertPathIsNotSymlink(dest, "install destination");
|
|
4629
|
+
const exists = await readdir5(dest).then(() => true).catch(() => false);
|
|
4317
4630
|
if (exists && !options.force) {
|
|
4318
4631
|
throw new Error(`Folder already exists at ${dest}. Call install_skill with force=true to overwrite after reviewing local changes.`);
|
|
4319
4632
|
}
|
|
4320
4633
|
if (exists) await rm3(dest, { recursive: true, force: true });
|
|
4321
|
-
const temp = await mkdtemp(
|
|
4634
|
+
const temp = await mkdtemp(join14(tmpdir4(), `floom-mcp-${parsed.slug}-`));
|
|
4322
4635
|
try {
|
|
4323
4636
|
await extractBundle(bundle, temp);
|
|
4324
4637
|
await rename3(temp, dest);
|
|
@@ -4341,24 +4654,24 @@ async function installViaApi(token, refText, target, options = {}) {
|
|
|
4341
4654
|
return { path: dest, version: dl.version, ref: info.ref ?? ref, has_scripts: !!dl.has_scripts };
|
|
4342
4655
|
}
|
|
4343
4656
|
async function parseSkillBundle(bundle) {
|
|
4344
|
-
const tmp = await mkdtemp(
|
|
4657
|
+
const tmp = await mkdtemp(join14(tmpdir4(), "floom-mcp-read-"));
|
|
4345
4658
|
try {
|
|
4346
4659
|
await extractBundle(bundle, tmp);
|
|
4347
4660
|
const files = [];
|
|
4348
4661
|
const walk2 = async (dir, rel = "") => {
|
|
4349
|
-
const entries = await
|
|
4662
|
+
const entries = await readdir5(dir, { withFileTypes: true });
|
|
4350
4663
|
for (const entry of entries) {
|
|
4351
4664
|
const nextRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
4352
|
-
const full =
|
|
4665
|
+
const full = join14(dir, entry.name);
|
|
4353
4666
|
if (entry.isDirectory()) await walk2(full, nextRel);
|
|
4354
4667
|
else files.push(nextRel);
|
|
4355
4668
|
}
|
|
4356
4669
|
};
|
|
4357
4670
|
await walk2(tmp);
|
|
4358
4671
|
const skillMdPath = files.find((f) => f.toUpperCase() === "SKILL.MD");
|
|
4359
|
-
const skillMd = skillMdPath ? await readFile10(
|
|
4672
|
+
const skillMd = skillMdPath ? await readFile10(join14(tmp, skillMdPath), "utf8") : "";
|
|
4360
4673
|
const skillJsonPath = files.find((f) => f.toLowerCase() === "skill.json");
|
|
4361
|
-
const skillJson = skillJsonPath ? JSON.parse(await readFile10(
|
|
4674
|
+
const skillJson = skillJsonPath ? JSON.parse(await readFile10(join14(tmp, skillJsonPath), "utf8")) : null;
|
|
4362
4675
|
return { files, skill_md: skillMd, skill_json: skillJson };
|
|
4363
4676
|
} finally {
|
|
4364
4677
|
await rm3(tmp, { recursive: true, force: true });
|
|
@@ -4451,11 +4764,29 @@ async function mcpCommand() {
|
|
|
4451
4764
|
}
|
|
4452
4765
|
|
|
4453
4766
|
// src/commands/doctor.ts
|
|
4454
|
-
import { mkdtemp as mkdtemp2, readdir as
|
|
4455
|
-
import { tmpdir as
|
|
4456
|
-
import { join as
|
|
4767
|
+
import { mkdtemp as mkdtemp2, readdir as readdir6, rm as rm4 } from "node:fs/promises";
|
|
4768
|
+
import { tmpdir as tmpdir5 } from "node:os";
|
|
4769
|
+
import { join as join15 } from "node:path";
|
|
4770
|
+
import { fileURLToPath } from "node:url";
|
|
4457
4771
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
4458
4772
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
4773
|
+
|
|
4774
|
+
// src/lib/api-health.ts
|
|
4775
|
+
async function probeApiHealth(rawApiUrl) {
|
|
4776
|
+
const apiV1 = trustedApiUrlOrDefault(rawApiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
|
|
4777
|
+
const healthUrl = `${apiV1}/health`;
|
|
4778
|
+
try {
|
|
4779
|
+
const res = await fetch(healthUrl, { method: "GET", signal: AbortSignal.timeout(8e3) });
|
|
4780
|
+
if (!res.ok) return { name: "api_health", ok: false, detail: `GET ${healthUrl} \u2192 HTTP ${res.status}` };
|
|
4781
|
+
const body = await res.json();
|
|
4782
|
+
if (body.ok !== true) return { name: "api_health", ok: false, detail: `unexpected JSON from ${healthUrl}` };
|
|
4783
|
+
return { name: "api_health", ok: true, detail: healthUrl };
|
|
4784
|
+
} catch (e) {
|
|
4785
|
+
return { name: "api_health", ok: false, detail: `${healthUrl}: ${e.message}` };
|
|
4786
|
+
}
|
|
4787
|
+
}
|
|
4788
|
+
|
|
4789
|
+
// src/commands/doctor.ts
|
|
4459
4790
|
function textOf(result) {
|
|
4460
4791
|
return String(result?.content?.[0]?.text ?? "");
|
|
4461
4792
|
}
|
|
@@ -4519,11 +4850,13 @@ async function doctorCommand(opts = {}) {
|
|
|
4519
4850
|
authCheck2 = fail("auth", `saved login rejected by API: ${e.message}`);
|
|
4520
4851
|
}
|
|
4521
4852
|
}
|
|
4853
|
+
const resolvedApiUrl2 = process.env.FLOOM_API_URL ?? auth2?.apiUrl ?? DEFAULT_API_URL;
|
|
4522
4854
|
const checks2 = [
|
|
4523
4855
|
pass("cli_version", VERSION),
|
|
4524
4856
|
authCheck2,
|
|
4525
4857
|
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
4858
|
];
|
|
4859
|
+
checks2.push(await probeApiHealth(resolvedApiUrl2));
|
|
4527
4860
|
emitDoctor(checks2, opts.json);
|
|
4528
4861
|
if (checks2.some((check) => !check.ok)) process.exit(1);
|
|
4529
4862
|
return;
|
|
@@ -4531,18 +4864,20 @@ async function doctorCommand(opts = {}) {
|
|
|
4531
4864
|
const checks = [];
|
|
4532
4865
|
const auth = await readAuth();
|
|
4533
4866
|
const token = process.env.FLOOM_API_TOKEN?.trim() || auth?.token;
|
|
4867
|
+
const resolvedApiUrl = process.env.FLOOM_API_URL ?? auth?.apiUrl ?? DEFAULT_API_URL;
|
|
4868
|
+
checks.push(await probeApiHealth(resolvedApiUrl));
|
|
4534
4869
|
const authCheck = await validateCurrentToken(token);
|
|
4535
4870
|
const hasValidToken = authCheck.ok && authCheck.status !== "warn" && Boolean(token);
|
|
4536
4871
|
checks.push(authCheck);
|
|
4537
|
-
const cliPath =
|
|
4872
|
+
const cliPath = fileURLToPath(import.meta.url);
|
|
4538
4873
|
if (!cliPath) {
|
|
4539
|
-
checks.push(fail("fresh_agent_cli_path", "
|
|
4874
|
+
checks.push(fail("fresh_agent_cli_path", "current CLI module path is empty"));
|
|
4540
4875
|
emitDoctor(checks, opts.json);
|
|
4541
4876
|
process.exit(1);
|
|
4542
4877
|
}
|
|
4543
|
-
const tmpHome = await mkdtemp2(
|
|
4544
|
-
const tmpSkills = await mkdtemp2(
|
|
4545
|
-
const tmpProject = await mkdtemp2(
|
|
4878
|
+
const tmpHome = await mkdtemp2(join15(tmpdir5(), "floom-doctor-home-"));
|
|
4879
|
+
const tmpSkills = await mkdtemp2(join15(tmpdir5(), "floom-doctor-skills-"));
|
|
4880
|
+
const tmpProject = await mkdtemp2(join15(tmpdir5(), "floom-doctor-project-"));
|
|
4546
4881
|
const transport = new StdioClientTransport({
|
|
4547
4882
|
command: process.execPath,
|
|
4548
4883
|
args: [cliPath, "mcp"],
|
|
@@ -4582,7 +4917,7 @@ async function doctorCommand(opts = {}) {
|
|
|
4582
4917
|
const skill = await client.callTool({ name: "get_skill", arguments: { ref: opts.ref } });
|
|
4583
4918
|
checks.push(/SKILL\.md/.test(textOf(skill)) ? pass("mcp_get_skill", opts.ref) : fail("mcp_get_skill", "bundle did not include SKILL.md"));
|
|
4584
4919
|
const installed = await client.callTool({ name: "install_skill", arguments: { ref: opts.ref, target: opts.target ?? "codex" } });
|
|
4585
|
-
const entries = await
|
|
4920
|
+
const entries = await readdir6(tmpSkills);
|
|
4586
4921
|
checks.push(entries.length > 0 ? pass("mcp_install_skill", textOf(installed)) : fail("mcp_install_skill", "target directory is empty"));
|
|
4587
4922
|
} else {
|
|
4588
4923
|
checks.push(warn("mcp_public_skill", "skipped; pass --ref <public-skill-ref> to verify get_skill/install_skill against a known public skill"));
|
|
@@ -4622,8 +4957,8 @@ program.command("whoami").description("Show the logged-in user.").action(whoamiC
|
|
|
4622
4957
|
program.command("init").description("Scaffold a new skill in the current directory.").action(initCommand);
|
|
4623
4958
|
program.command("validate").description("Validate the skill in the current directory.").option("--json", "Emit machine-readable JSON").action((opts) => validateCommand(opts));
|
|
4624
4959
|
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>", "
|
|
4960
|
+
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("--concurrency <n>", "Bulk push concurrency, 1-16", "6").addHelpText("after", "\nExamples:\n $ floom push\n $ floom push ./skills --workspace team-workspace --concurrency 4").action((dir, opts) => pushCommand(dir ?? ".", opts));
|
|
4961
|
+
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
4962
|
program.command("installed").description("List installed skills in this project.").option("--json").action(installedCommand);
|
|
4628
4963
|
program.command("outdated").description("Show installed skills with newer versions available.").action(outdatedCommand);
|
|
4629
4964
|
program.command("update [ref]").description("Update installed skills to latest.").option("--force", "Overwrite local edits").action((ref, opts) => updateCommand(ref, opts));
|
|
@@ -4636,6 +4971,8 @@ program.command("pinned").alias("pins").description("List workspace skills pinne
|
|
|
4636
4971
|
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
4972
|
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
4973
|
program.command("unshare <ref> <email>").description("Revoke someone's access.").action((ref, email) => unshareCommand(ref, email));
|
|
4974
|
+
var linkCmd = program.command("link").description("Create opaque share links for unlisted/public skills");
|
|
4975
|
+
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
4976
|
var configCmd = program.command("config").description("Manage local Floom configuration");
|
|
4640
4977
|
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
4978
|
function addWorkspaceCommands(cmd) {
|
|
@@ -4694,4 +5031,3 @@ bcryptjs/dist/bcrypt.js:
|
|
|
4694
5031
|
* see: https://github.com/dcodeIO/bcrypt.js for details
|
|
4695
5032
|
*)
|
|
4696
5033
|
*/
|
|
4697
|
-
//# sourceMappingURL=index.js.map
|