@floomhq/floom 1.0.64 → 2.0.0
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.d.ts +1 -0
- package/dist/index.js +3663 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -25
- package/package.json +37 -45
- package/LICENSE +0 -21
- package/README.md +0 -90
- package/bin/floom.js +0 -2
- package/dist/audit.js +0 -236
- package/dist/cli.js +0 -1313
- package/dist/config.js +0 -85
- package/dist/daemon.js +0 -450
- package/dist/delete.js +0 -55
- package/dist/doctor.js +0 -381
- package/dist/errors.js +0 -71
- package/dist/feedback.js +0 -34
- package/dist/info.js +0 -78
- package/dist/init.js +0 -221
- package/dist/install.js +0 -305
- package/dist/launch.js +0 -110
- package/dist/lib/api.js +0 -142
- package/dist/lib/skill-labels.js +0 -140
- package/dist/library.js +0 -102
- package/dist/list.js +0 -79
- package/dist/login.js +0 -259
- package/dist/mcp.js +0 -20
- package/dist/package.js +0 -507
- package/dist/publish.js +0 -240
- package/dist/push-watch.js +0 -372
- package/dist/scan.js +0 -24
- package/dist/search.js +0 -54
- package/dist/secrets.js +0 -119
- package/dist/setup.js +0 -301
- package/dist/share.js +0 -70
- package/dist/status.js +0 -181
- package/dist/sync-manifest.js +0 -314
- package/dist/sync.js +0 -581
- package/dist/targets.js +0 -49
- package/dist/ui.js +0 -28
- package/dist/whoami.js +0 -64
package/dist/doctor.js
DELETED
|
@@ -1,381 +0,0 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
2
|
-
import { execFile as execFileCb } from "node:child_process";
|
|
3
|
-
import { delimiter } from "node:path";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { stat, readFile, access, readdir, constants, realpath } from "node:fs/promises";
|
|
6
|
-
import { promisify } from "node:util";
|
|
7
|
-
import { readConfig, refreshConfigIfNeeded, CONFIG_PATH, resolveApiUrl } from "./config.js";
|
|
8
|
-
import { floomFetch } from "./lib/api.js";
|
|
9
|
-
import { c, symbols } from "./ui.js";
|
|
10
|
-
import { CLI_VERSION, compareSemverish, formatVersionLabel } from "./version.js";
|
|
11
|
-
import { targetSkillsDir } from "./targets.js";
|
|
12
|
-
const execFile = promisify(execFileCb);
|
|
13
|
-
function statusBadge(s) {
|
|
14
|
-
if (s === "ok")
|
|
15
|
-
return c.green(symbols.ok);
|
|
16
|
-
if (s === "warn")
|
|
17
|
-
return c.yellow("!");
|
|
18
|
-
return c.red(symbols.fail);
|
|
19
|
-
}
|
|
20
|
-
async function checkAuth() {
|
|
21
|
-
const cfg = await readConfig();
|
|
22
|
-
if (!cfg) {
|
|
23
|
-
return {
|
|
24
|
-
name: "Auth",
|
|
25
|
-
status: "ok",
|
|
26
|
-
detail: "Receiver mode ready. Sign in only when publishing or listing your own skills.",
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
const apiUrl = resolveApiUrl(cfg);
|
|
30
|
-
try {
|
|
31
|
-
const res = await floomFetch(`${apiUrl}/api/me`, "check authentication", {
|
|
32
|
-
token: cfg.accessToken,
|
|
33
|
-
checkOk: false,
|
|
34
|
-
});
|
|
35
|
-
if (res.status === 401) {
|
|
36
|
-
const refreshed = await refreshConfigIfNeeded(cfg, { force: true });
|
|
37
|
-
if (refreshed.accessToken !== cfg.accessToken) {
|
|
38
|
-
const retry = await floomFetch(`${apiUrl}/api/me`, "check authentication", {
|
|
39
|
-
token: refreshed.accessToken,
|
|
40
|
-
checkOk: false,
|
|
41
|
-
});
|
|
42
|
-
if (retry.ok) {
|
|
43
|
-
const data = (await retry.json());
|
|
44
|
-
return {
|
|
45
|
-
name: "Auth",
|
|
46
|
-
status: "ok",
|
|
47
|
-
detail: data.email ? `Signed in as ${data.email} (refreshed)` : "Signed in (refreshed)",
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return {
|
|
52
|
-
name: "Auth",
|
|
53
|
-
status: "fail",
|
|
54
|
-
detail: "Token rejected (401).",
|
|
55
|
-
hint: "Run `npx -y @floomhq/floom logout && npx -y @floomhq/floom login` to refresh.",
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
if (!res.ok) {
|
|
59
|
-
return {
|
|
60
|
-
name: "Auth",
|
|
61
|
-
status: "warn",
|
|
62
|
-
detail: `API returned ${res.status}.`,
|
|
63
|
-
hint: "Service may be degraded; try again shortly.",
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
const data = (await res.json());
|
|
67
|
-
return {
|
|
68
|
-
name: "Auth",
|
|
69
|
-
status: "ok",
|
|
70
|
-
detail: data.email ? `Signed in as ${data.email}` : "Signed in",
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
catch (err) {
|
|
74
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
75
|
-
return {
|
|
76
|
-
name: "Auth",
|
|
77
|
-
status: "fail",
|
|
78
|
-
detail: `Cannot reach ${apiUrl}: ${msg}`,
|
|
79
|
-
hint: "Check your network. Try `curl " + apiUrl + "/api/me`.",
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
async function checkMcp() {
|
|
84
|
-
const candidates = [
|
|
85
|
-
{ name: "Claude Code", path: join(homedir(), ".claude.json") },
|
|
86
|
-
{ name: "Claude Code (settings)", path: join(homedir(), ".claude", "settings.json") },
|
|
87
|
-
{ name: "Codex", path: join(homedir(), ".codex", "config.toml") },
|
|
88
|
-
{ name: "Cursor", path: join(homedir(), ".cursor", "mcp.json") },
|
|
89
|
-
{ name: "OpenCode", path: join(homedir(), ".config", "opencode", "opencode.json") },
|
|
90
|
-
{ name: "Kimi", path: join(homedir(), ".kimi", "mcp.json") },
|
|
91
|
-
{ name: "Gemini", path: join(homedir(), ".gemini", "settings.json") },
|
|
92
|
-
];
|
|
93
|
-
const found = [];
|
|
94
|
-
for (const cand of candidates) {
|
|
95
|
-
try {
|
|
96
|
-
const buf = await readFile(cand.path, "utf8");
|
|
97
|
-
// Cheap match: any reference to a Floom MCP server. The actual MCP
|
|
98
|
-
// package name is @floomhq/floom-mcp-sync. Detect by package name OR
|
|
99
|
-
// by "floom" + "mcp_servers"/"mcpServers" co-occurrence.
|
|
100
|
-
const hasFloom = buf.includes("@floomhq/floom-mcp-sync") ||
|
|
101
|
-
(buf.includes("\"floom\"") && (buf.includes("mcpServers") || buf.includes("mcp_servers") || buf.includes("\"mcp\""))) ||
|
|
102
|
-
(buf.includes("[mcp_servers.floom]"));
|
|
103
|
-
if (hasFloom)
|
|
104
|
-
found.push({ name: cand.name, configPath: cand.path, hasFloom });
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
// file missing — fine
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
if (found.length === 0) {
|
|
111
|
-
return {
|
|
112
|
-
name: "MCP",
|
|
113
|
-
status: "ok",
|
|
114
|
-
detail: "Optional MCP not registered. `npx -y @floomhq/floom add` still writes local skill files.",
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
return {
|
|
118
|
-
name: "MCP",
|
|
119
|
-
status: "ok",
|
|
120
|
-
detail: `Registered with: ${found.map((f) => f.name).join(", ")}`,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
async function checkTargetDir(target) {
|
|
124
|
-
const dir = targetSkillsDir(target);
|
|
125
|
-
try {
|
|
126
|
-
const s = await stat(dir);
|
|
127
|
-
if (!s.isDirectory()) {
|
|
128
|
-
return {
|
|
129
|
-
name: "Target dir",
|
|
130
|
-
status: "fail",
|
|
131
|
-
detail: `${dir} exists but is not a directory.`,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
try {
|
|
135
|
-
await access(dir, constants.W_OK);
|
|
136
|
-
return { name: "Target dir", status: "ok", detail: `${dir} (writable)` };
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
return {
|
|
140
|
-
name: "Target dir",
|
|
141
|
-
status: "fail",
|
|
142
|
-
detail: `${dir} is not writable.`,
|
|
143
|
-
hint: `Try: chmod u+w ${dir}`,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
catch (err) {
|
|
148
|
-
if (err.code === "ENOENT") {
|
|
149
|
-
return {
|
|
150
|
-
name: "Target dir",
|
|
151
|
-
status: "warn",
|
|
152
|
-
detail: `${dir} does not exist yet.`,
|
|
153
|
-
hint: `It will be created on first \`npx -y @floomhq/floom add <link> --target ${target}\`.`,
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
throw err;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
async function checkLastSync(target) {
|
|
160
|
-
const dir = targetSkillsDir(target);
|
|
161
|
-
try {
|
|
162
|
-
const entries = await readdir(dir);
|
|
163
|
-
const skills = entries.filter((e) => e.endsWith(".md") || !e.startsWith("."));
|
|
164
|
-
if (skills.length === 0) {
|
|
165
|
-
return {
|
|
166
|
-
name: "Last sync",
|
|
167
|
-
status: "warn",
|
|
168
|
-
detail: "No synced skills found yet.",
|
|
169
|
-
hint: `Run \`npx -y @floomhq/floom add <link> --target ${target}\` to install your first skill.`,
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
// Find most recently modified entry
|
|
173
|
-
let newest = { name: "", mtime: 0 };
|
|
174
|
-
for (const name of skills) {
|
|
175
|
-
try {
|
|
176
|
-
const s = await stat(join(dir, name));
|
|
177
|
-
if (s.mtimeMs > newest.mtime)
|
|
178
|
-
newest = { name, mtime: s.mtimeMs };
|
|
179
|
-
}
|
|
180
|
-
catch {
|
|
181
|
-
// skip
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
if (newest.mtime === 0) {
|
|
185
|
-
return {
|
|
186
|
-
name: "Last sync",
|
|
187
|
-
status: "warn",
|
|
188
|
-
detail: `${skills.length} entries, no readable mtime.`,
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
const ageSec = Math.round((Date.now() - newest.mtime) / 1000);
|
|
192
|
-
const human = ageSec < 60
|
|
193
|
-
? `${ageSec}s ago`
|
|
194
|
-
: ageSec < 3600
|
|
195
|
-
? `${Math.round(ageSec / 60)}m ago`
|
|
196
|
-
: ageSec < 86400
|
|
197
|
-
? `${Math.round(ageSec / 3600)}h ago`
|
|
198
|
-
: `${Math.round(ageSec / 86400)}d ago`;
|
|
199
|
-
return {
|
|
200
|
-
name: "Last sync",
|
|
201
|
-
status: "ok",
|
|
202
|
-
detail: `${newest.name} — ${human} (${skills.length} total)`,
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
catch (err) {
|
|
206
|
-
if (err.code === "ENOENT") {
|
|
207
|
-
return {
|
|
208
|
-
name: "Last sync",
|
|
209
|
-
status: "warn",
|
|
210
|
-
detail: "Skills dir not created yet.",
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
return {
|
|
214
|
-
name: "Last sync",
|
|
215
|
-
status: "warn",
|
|
216
|
-
detail: `Cannot read skills dir: ${err.message}`,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
async function checkVersion() {
|
|
221
|
-
const apiUrl = resolveApiUrl(await readConfig());
|
|
222
|
-
try {
|
|
223
|
-
const res = await floomFetch(`${apiUrl}/api/v1/cli-version`, "check CLI version", {
|
|
224
|
-
headers: { accept: "application/json" },
|
|
225
|
-
checkOk: false,
|
|
226
|
-
});
|
|
227
|
-
if (!res.ok) {
|
|
228
|
-
// Endpoint optional — treat as info-only, not a failure
|
|
229
|
-
return {
|
|
230
|
-
name: "Version",
|
|
231
|
-
status: "ok",
|
|
232
|
-
detail: `CLI ${formatVersionLabel(CLI_VERSION)} (server check skipped)`,
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
const data = (await res.json());
|
|
236
|
-
if (data.min && compareSemverish(CLI_VERSION, data.min) < 0) {
|
|
237
|
-
return {
|
|
238
|
-
name: "Version",
|
|
239
|
-
status: "fail",
|
|
240
|
-
detail: `CLI ${formatVersionLabel(CLI_VERSION)} below required ${formatVersionLabel(data.min)}.`,
|
|
241
|
-
hint: "Run `npm i -g @floomhq/floom` to upgrade, then use `floom`.",
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
if (data.latest && compareSemverish(CLI_VERSION, data.latest) < 0) {
|
|
245
|
-
return {
|
|
246
|
-
name: "Version",
|
|
247
|
-
status: "warn",
|
|
248
|
-
detail: `CLI ${formatVersionLabel(CLI_VERSION)}, latest is ${formatVersionLabel(data.latest)}.`,
|
|
249
|
-
hint: "Run `npm i -g @floomhq/floom` to upgrade, then use `floom`.",
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
return { name: "Version", status: "ok", detail: `CLI ${formatVersionLabel(CLI_VERSION)} (current)` };
|
|
253
|
-
}
|
|
254
|
-
catch {
|
|
255
|
-
return {
|
|
256
|
-
name: "Version",
|
|
257
|
-
status: "ok",
|
|
258
|
-
detail: `CLI ${formatVersionLabel(CLI_VERSION)} (offline)`,
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
async function findPathExecutables(name) {
|
|
263
|
-
const seen = new Set();
|
|
264
|
-
const out = [];
|
|
265
|
-
const pathDirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
266
|
-
for (const dir of pathDirs) {
|
|
267
|
-
const candidate = join(dir, name);
|
|
268
|
-
if (seen.has(candidate))
|
|
269
|
-
continue;
|
|
270
|
-
seen.add(candidate);
|
|
271
|
-
try {
|
|
272
|
-
await access(candidate, constants.X_OK);
|
|
273
|
-
out.push(candidate);
|
|
274
|
-
}
|
|
275
|
-
catch {
|
|
276
|
-
// not present or not executable
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
return out;
|
|
280
|
-
}
|
|
281
|
-
async function safeRealpath(path) {
|
|
282
|
-
if (!path)
|
|
283
|
-
return null;
|
|
284
|
-
try {
|
|
285
|
-
return await realpath(path);
|
|
286
|
-
}
|
|
287
|
-
catch {
|
|
288
|
-
return null;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
async function checkCliCommand() {
|
|
292
|
-
const bins = await findPathExecutables("floom");
|
|
293
|
-
if (bins.length === 0) {
|
|
294
|
-
return {
|
|
295
|
-
name: "CLI command",
|
|
296
|
-
status: "ok",
|
|
297
|
-
detail: "`floom` is not globally installed. `npx -y @floomhq/floom ...` is ready.",
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
const active = bins[0] ?? "";
|
|
301
|
-
const activeReal = await safeRealpath(active);
|
|
302
|
-
const currentReal = await safeRealpath(process.argv[1]);
|
|
303
|
-
if (activeReal && currentReal && activeReal === currentReal) {
|
|
304
|
-
return {
|
|
305
|
-
name: "CLI command",
|
|
306
|
-
status: "ok",
|
|
307
|
-
detail: `floom resolves to ${active}`,
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
const activeText = await readExecutableSample(active);
|
|
311
|
-
const activeVersion = await readExecutableVersion(active);
|
|
312
|
-
const looksLikeOldRuntime = active.includes(".floom/repo") ||
|
|
313
|
-
active.includes("/skills-minimal/") ||
|
|
314
|
-
activeText.includes("unknown command") ||
|
|
315
|
-
activeText.includes("Floom runtime") ||
|
|
316
|
-
(activeVersion !== null && compareSemverish(activeVersion, CLI_VERSION) < 0);
|
|
317
|
-
return {
|
|
318
|
-
name: "CLI command",
|
|
319
|
-
status: looksLikeOldRuntime ? "warn" : "ok",
|
|
320
|
-
detail: looksLikeOldRuntime
|
|
321
|
-
? `Another floom command is first on PATH: ${active}${activeVersion ? ` (${formatVersionLabel(activeVersion)})` : ""}`
|
|
322
|
-
: `floom resolves to ${active}`,
|
|
323
|
-
...(looksLikeOldRuntime
|
|
324
|
-
? {
|
|
325
|
-
hint: `If \`floom doctor\` opens the old runtime CLI, remove that file or move it later in PATH, then run \`npm i -g @floomhq/floom\`. Inspect with: command -v floom`,
|
|
326
|
-
}
|
|
327
|
-
: {}),
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
async function readExecutableSample(path) {
|
|
331
|
-
try {
|
|
332
|
-
return (await readFile(path, "utf8")).slice(0, 4000);
|
|
333
|
-
}
|
|
334
|
-
catch {
|
|
335
|
-
return "";
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
async function readExecutableVersion(path) {
|
|
339
|
-
try {
|
|
340
|
-
const { stdout, stderr } = await execFile(path, ["--version"], { timeout: 1500 });
|
|
341
|
-
const text = `${stdout}\n${stderr}`;
|
|
342
|
-
return text.match(/\d+\.\d+\.\d+(?:[-+][A-Za-z0-9._-]+)?/)?.[0] ?? null;
|
|
343
|
-
}
|
|
344
|
-
catch {
|
|
345
|
-
return null;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
export async function doctor(opts = {}) {
|
|
349
|
-
const target = opts.target ?? "claude";
|
|
350
|
-
process.stdout.write(`\n${c.bold("floom doctor")} ${c.dim(`(${formatVersionLabel(CLI_VERSION)}, target: ${target})`)}\n\n`);
|
|
351
|
-
const checks = await Promise.all([
|
|
352
|
-
checkCliCommand(),
|
|
353
|
-
checkAuth(),
|
|
354
|
-
checkMcp(),
|
|
355
|
-
checkTargetDir(target),
|
|
356
|
-
checkLastSync(target),
|
|
357
|
-
checkVersion(),
|
|
358
|
-
]);
|
|
359
|
-
// Compute column widths for clean table output.
|
|
360
|
-
const nameW = Math.max(...checks.map((c) => c.name.length), 6);
|
|
361
|
-
for (const check of checks) {
|
|
362
|
-
const padded = check.name.padEnd(nameW);
|
|
363
|
-
process.stdout.write(` ${statusBadge(check.status)} ${c.bold(padded)} ${check.detail}\n`);
|
|
364
|
-
if (check.hint && check.status !== "ok") {
|
|
365
|
-
process.stdout.write(` ${c.dim("→ " + check.hint)}\n`);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
const anyFail = checks.some((c) => c.status === "fail");
|
|
369
|
-
const anyWarn = checks.some((c) => c.status === "warn");
|
|
370
|
-
process.stdout.write("\n");
|
|
371
|
-
if (anyFail) {
|
|
372
|
-
process.stdout.write(` ${c.red("✗ Some checks failed.")} See hints above.\n\n`);
|
|
373
|
-
process.exit(1);
|
|
374
|
-
}
|
|
375
|
-
if (anyWarn) {
|
|
376
|
-
process.stdout.write(` ${c.yellow("! All critical checks passed, with warnings.")}\n\n`);
|
|
377
|
-
process.exit(0);
|
|
378
|
-
}
|
|
379
|
-
process.stdout.write(` ${c.green("✓ All checks passed.")} Floom is healthy.\n\n`);
|
|
380
|
-
process.stdout.write(` ${c.dim("Config: " + CONFIG_PATH)}\n\n`);
|
|
381
|
-
}
|
package/dist/errors.js
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { c, symbols } from "./ui.js";
|
|
2
|
-
/**
|
|
3
|
-
* FloomError is a friendly, user-facing error. Throw this instead of raw Error
|
|
4
|
-
* when you want a clean message (no stack trace, no "Error:" prefix).
|
|
5
|
-
*/
|
|
6
|
-
export class FloomError extends Error {
|
|
7
|
-
hint;
|
|
8
|
-
constructor(message, hint) {
|
|
9
|
-
super(message);
|
|
10
|
-
this.name = "FloomError";
|
|
11
|
-
if (hint)
|
|
12
|
-
this.hint = hint;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
export function friendlyHttp(status, action, detail) {
|
|
16
|
-
if (status === 401) {
|
|
17
|
-
return new FloomError("Your token expired.", "Run `npx -y @floomhq/floom login` to refresh.");
|
|
18
|
-
}
|
|
19
|
-
if (status === 403) {
|
|
20
|
-
return new FloomError(`You don't have permission to ${action}.`);
|
|
21
|
-
}
|
|
22
|
-
if (status === 404) {
|
|
23
|
-
if (/fetch|inspect|add|install|show|get|search|list|info/i.test(action)) {
|
|
24
|
-
return new FloomError("Skill not found.", "Check the link or slug, then try again.");
|
|
25
|
-
}
|
|
26
|
-
return new FloomError("Skill not found.", "Run `npx -y @floomhq/floom publish <path>` to create a new one.");
|
|
27
|
-
}
|
|
28
|
-
if (status === 413) {
|
|
29
|
-
return new FloomError("That file is too large to publish.");
|
|
30
|
-
}
|
|
31
|
-
if (status === 429) {
|
|
32
|
-
return new FloomError("Floom is rate limiting requests.", "The CLI retries rate-limited requests automatically. Try again in a moment if this persists.");
|
|
33
|
-
}
|
|
34
|
-
if (status >= 500) {
|
|
35
|
-
return new FloomError("Floom is having trouble right now.", "Try again in a moment.");
|
|
36
|
-
}
|
|
37
|
-
if (detail) {
|
|
38
|
-
return new FloomError(detail);
|
|
39
|
-
}
|
|
40
|
-
return new FloomError(`Request failed (HTTP ${status}) while trying to ${action}.`);
|
|
41
|
-
}
|
|
42
|
-
export function friendlyNetwork(err) {
|
|
43
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
44
|
-
if (/ENOTFOUND|EAI_AGAIN|ECONNREFUSED|ECONNRESET|ETIMEDOUT|fetch failed/i.test(msg)) {
|
|
45
|
-
return new FloomError("Couldn't reach floom (offline?).", "Try again in a moment.");
|
|
46
|
-
}
|
|
47
|
-
return new FloomError(msg);
|
|
48
|
-
}
|
|
49
|
-
export function printError(err, opts = {}) {
|
|
50
|
-
if (opts.json) {
|
|
51
|
-
const error = err instanceof FloomError
|
|
52
|
-
? { error: err.message, hint: err.hint ?? null }
|
|
53
|
-
: { error: err instanceof Error ? err.message : String(err), hint: null };
|
|
54
|
-
process.stderr.write(`${JSON.stringify(error)}\n`);
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
if (err instanceof FloomError) {
|
|
58
|
-
process.stderr.write(`\n${symbols.fail} ${err.message}\n`);
|
|
59
|
-
if (err.hint) {
|
|
60
|
-
for (const line of err.hint.split("\n")) {
|
|
61
|
-
process.stderr.write(` ${c.dim(line)}\n`);
|
|
62
|
-
}
|
|
63
|
-
process.stderr.write("\n");
|
|
64
|
-
}
|
|
65
|
-
else
|
|
66
|
-
process.stderr.write("\n");
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
70
|
-
process.stderr.write(`\n${symbols.fail} ${msg}\n\n`);
|
|
71
|
-
}
|
package/dist/feedback.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import ora from "ora";
|
|
2
|
-
import { readConfig, resolveApiUrl } from "./config.js";
|
|
3
|
-
import { floomFetch } from "./lib/api.js";
|
|
4
|
-
import { c, symbols } from "./ui.js";
|
|
5
|
-
export async function feedback(opts) {
|
|
6
|
-
const cfg = await readConfig();
|
|
7
|
-
const apiUrl = resolveApiUrl(cfg);
|
|
8
|
-
const payload = {
|
|
9
|
-
event_name: "feedback.submitted",
|
|
10
|
-
source: "cli",
|
|
11
|
-
props: {
|
|
12
|
-
kind: opts.kind,
|
|
13
|
-
message: opts.message,
|
|
14
|
-
...(opts.target ? { target: opts.target } : {}),
|
|
15
|
-
...(opts.skill ? { skill: opts.skill } : {}),
|
|
16
|
-
},
|
|
17
|
-
};
|
|
18
|
-
const spinner = opts.json ? null : ora({ text: c.dim("Sending feedback..."), color: "yellow" }).start();
|
|
19
|
-
try {
|
|
20
|
-
await floomFetch(`${apiUrl}/api/v1/events`, "send feedback", {
|
|
21
|
-
method: "POST",
|
|
22
|
-
...(cfg?.accessToken ? { token: cfg.accessToken } : {}),
|
|
23
|
-
body: payload,
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
finally {
|
|
27
|
-
spinner?.stop();
|
|
28
|
-
}
|
|
29
|
-
if (opts.json) {
|
|
30
|
-
process.stdout.write(`${JSON.stringify({ ok: true }, null, 2)}\n`);
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
process.stdout.write(`\n${symbols.ok} Feedback sent\n\n`);
|
|
34
|
-
}
|
package/dist/info.js
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import ora from "ora";
|
|
2
|
-
import { readConfig, resolveApiUrl } from "./config.js";
|
|
3
|
-
import { getJson } from "./lib/api.js";
|
|
4
|
-
import { extractRequires, formatToolList, formatType } from "./lib/skill-labels.js";
|
|
5
|
-
import { c, symbols } from "./ui.js";
|
|
6
|
-
import { FloomError } from "./errors.js";
|
|
7
|
-
const SLUG_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
8
|
-
function slugFromInput(input) {
|
|
9
|
-
const trimmed = input.trim();
|
|
10
|
-
try {
|
|
11
|
-
const url = new URL(trimmed);
|
|
12
|
-
const last = url.pathname.split("/").filter(Boolean).at(-1) ?? "";
|
|
13
|
-
return last.replace(/\.(md|json)$/i, "");
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
return trimmed.replace(/\.(md|json)$/i, "");
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
export async function info(opts) {
|
|
20
|
-
const slug = slugFromInput(opts.slug);
|
|
21
|
-
if (!slug)
|
|
22
|
-
throw new FloomError("Missing skill slug.", "Try: `npx -y @floomhq/floom info <slug>`");
|
|
23
|
-
if (!SLUG_RE.test(slug)) {
|
|
24
|
-
throw new FloomError(`Invalid skill slug: ${opts.slug}`, "Use a Floom skill slug or URL.");
|
|
25
|
-
}
|
|
26
|
-
const cfg = await readConfig();
|
|
27
|
-
const apiUrl = resolveApiUrl(cfg);
|
|
28
|
-
const spinner = opts.json ? null : ora({ text: c.dim(`Loading ${slug}...`), color: "yellow" }).start();
|
|
29
|
-
let detail;
|
|
30
|
-
try {
|
|
31
|
-
detail = await getJson(`${apiUrl}/api/v1/skills/${encodeURIComponent(slug)}`, "load skill metadata", cfg?.accessToken);
|
|
32
|
-
}
|
|
33
|
-
finally {
|
|
34
|
-
spinner?.stop();
|
|
35
|
-
}
|
|
36
|
-
if (opts.json) {
|
|
37
|
-
// Strip body_md from JSON output to keep it summary-shaped, but expose
|
|
38
|
-
// the parsed `requires` list so scripted consumers don't need to parse
|
|
39
|
-
// YAML themselves.
|
|
40
|
-
const { body_md: _body, ...summary } = detail;
|
|
41
|
-
const requires = extractRequires(detail.body_md);
|
|
42
|
-
process.stdout.write(`${JSON.stringify({ ...summary, requires }, null, 2)}\n`);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
const title = detail.title ?? c.dim("(untitled)");
|
|
46
|
-
process.stdout.write(`\n${symbols.dot} ${c.bold(title)}\n\n`);
|
|
47
|
-
process.stdout.write(` ${c.dim("Slug: ")}${detail.slug}\n`);
|
|
48
|
-
if (detail.description) {
|
|
49
|
-
process.stdout.write(` ${c.dim("About: ")}${detail.description}\n`);
|
|
50
|
-
}
|
|
51
|
-
process.stdout.write(` ${c.dim("Visibility: ")}${detail.visibility}\n`);
|
|
52
|
-
process.stdout.write(` ${c.dim("Type: ")}${formatType(detail.asset_type)}\n`);
|
|
53
|
-
const requires = extractRequires(detail.body_md);
|
|
54
|
-
if (requires.length > 0) {
|
|
55
|
-
process.stdout.write(` ${c.dim("Needs: ")}${formatToolList(requires)}\n`);
|
|
56
|
-
}
|
|
57
|
-
if (detail.version) {
|
|
58
|
-
process.stdout.write(` ${c.dim("Version: ")}${detail.version}\n`);
|
|
59
|
-
}
|
|
60
|
-
if (detail.installs_as) {
|
|
61
|
-
process.stdout.write(` ${c.dim("Installs: ")}${detail.installs_as}\n`);
|
|
62
|
-
}
|
|
63
|
-
if (detail.original_filename) {
|
|
64
|
-
process.stdout.write(` ${c.dim("File: ")}${detail.original_filename}\n`);
|
|
65
|
-
}
|
|
66
|
-
if (detail.owner_email) {
|
|
67
|
-
process.stdout.write(` ${c.dim("Owner: ")}${detail.owner_email}\n`);
|
|
68
|
-
}
|
|
69
|
-
if (typeof detail.save_count === "number") {
|
|
70
|
-
process.stdout.write(` ${c.dim("Saves: ")}${detail.save_count}\n`);
|
|
71
|
-
}
|
|
72
|
-
if (detail.shared_with_emails?.length) {
|
|
73
|
-
process.stdout.write(` ${c.dim("Shared: ")}${detail.shared_with_emails.join(", ")}\n`);
|
|
74
|
-
}
|
|
75
|
-
process.stdout.write(` ${c.dim("Created: ")}${detail.created_at}\n`);
|
|
76
|
-
process.stdout.write(` ${c.dim("Updated: ")}${detail.updated_at}\n`);
|
|
77
|
-
process.stdout.write(` ${c.dim("URL: ")}${c.cyan(detail.url.replace(/\.md$/, ""))}\n\n`);
|
|
78
|
-
}
|