@floomhq/floom 1.0.64 → 2.0.1

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/config.js DELETED
@@ -1,85 +0,0 @@
1
- import { homedir } from "node:os";
2
- import { dirname, join } from "node:path";
3
- import { mkdir, readFile, writeFile, chmod, unlink } from "node:fs/promises";
4
- export const CONFIG_DIR = join(homedir(), ".floom");
5
- export const CONFIG_PATH = join(CONFIG_DIR, "config.json");
6
- export const DEFAULT_API_URL = "https://floom.dev";
7
- export const DEFAULT_WEB_URL = "https://floom.dev";
8
- export function getApiUrl() {
9
- return process.env.FLOOM_API_URL?.replace(/\/$/, "") ?? DEFAULT_API_URL;
10
- }
11
- export function resolveApiUrl(cfg) {
12
- return process.env.FLOOM_API_URL?.replace(/\/$/, "") ?? cfg?.apiUrl?.replace(/\/$/, "") ?? DEFAULT_API_URL;
13
- }
14
- export function getWebUrl() {
15
- return process.env.FLOOM_WEB_URL?.replace(/\/$/, "") ?? DEFAULT_WEB_URL;
16
- }
17
- export async function readConfig() {
18
- try {
19
- const buf = await readFile(process.env.FLOOM_CONFIG_PATH ?? CONFIG_PATH, "utf8");
20
- const parsed = JSON.parse(buf);
21
- if (!parsed.accessToken || !parsed.refreshToken)
22
- return null;
23
- return refreshConfigIfNeeded(parsed);
24
- }
25
- catch (e) {
26
- if (e.code === "ENOENT")
27
- return null;
28
- throw e;
29
- }
30
- }
31
- export async function refreshConfigIfNeeded(cfg, opts = {}) {
32
- const now = Math.floor(Date.now() / 1000);
33
- if (!opts.force && (typeof cfg.expiresAt !== "number" || cfg.expiresAt > now + 120))
34
- return cfg;
35
- try {
36
- const refreshed = await refreshConfig(cfg);
37
- await writeConfig(refreshed);
38
- return refreshed;
39
- }
40
- catch {
41
- return cfg;
42
- }
43
- }
44
- async function refreshConfig(cfg) {
45
- const apiUrl = resolveApiUrl(cfg);
46
- const res = await fetch(`${apiUrl}/api/auth/refresh`, {
47
- method: "POST",
48
- headers: { "content-type": "application/json" },
49
- body: JSON.stringify({ refresh_token: cfg.refreshToken }),
50
- });
51
- if (!res.ok)
52
- throw new Error(`refresh failed with ${res.status}`);
53
- const data = (await res.json());
54
- if (!data.access_token || !data.refresh_token)
55
- throw new Error("refresh response missing tokens");
56
- const expiresIn = Number(data.expires_in ?? "3600");
57
- return {
58
- ...cfg,
59
- accessToken: data.access_token,
60
- refreshToken: data.refresh_token,
61
- expiresAt: Math.floor(Date.now() / 1000) + (Number.isFinite(expiresIn) ? expiresIn : 3600),
62
- ...(typeof data.email === "string" || data.email === null
63
- ? { email: data.email }
64
- : typeof cfg.email === "string" || cfg.email === null
65
- ? { email: cfg.email }
66
- : {}),
67
- };
68
- }
69
- export async function writeConfig(cfg) {
70
- const targetPath = process.env.FLOOM_CONFIG_PATH ?? CONFIG_PATH;
71
- const targetDir = dirname(targetPath);
72
- await mkdir(targetDir, { recursive: true, mode: 0o700 });
73
- await writeFile(targetPath, JSON.stringify(cfg, null, 2), { mode: 0o600 });
74
- // belt-and-suspenders: set perms in case the file already existed
75
- await chmod(targetPath, 0o600);
76
- }
77
- export async function deleteConfig() {
78
- try {
79
- await unlink(process.env.FLOOM_CONFIG_PATH ?? CONFIG_PATH);
80
- }
81
- catch (e) {
82
- if (e.code !== "ENOENT")
83
- throw e;
84
- }
85
- }
package/dist/daemon.js DELETED
@@ -1,450 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { access, appendFile, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
3
- import { homedir, hostname, platform } from "node:os";
4
- import { dirname, join } from "node:path";
5
- import { CONFIG_DIR } from "./config.js";
6
- import { CLI_VERSION } from "./version.js";
7
- import { FloomError } from "./errors.js";
8
- import { c, symbols } from "./ui.js";
9
- import { AGENT_TARGETS, isAgentTarget, targetSkillsDirEnv } from "./targets.js";
10
- const SERVICE_NAME = "floom-sync.service";
11
- const LAUNCHD_LABEL = "dev.floom.sync";
12
- const LOG_PATH = join(CONFIG_DIR, "daemon.log");
13
- const STATUS_PATH = join(CONFIG_DIR, "daemon-status.json");
14
- const NATIVE_BASELINE_VERSION = 4;
15
- const MIN_INTERVAL_SECONDS = 30;
16
- const MIN_TIMEOUT_SECONDS = 30;
17
- function targetsFor(value) {
18
- return value === "all" ? [...AGENT_TARGETS] : [value];
19
- }
20
- function cliInvocation() {
21
- const override = process.env.FLOOM_DAEMON_CLI_COMMAND?.trim();
22
- if (override)
23
- return override;
24
- return `npx -y @floomhq/floom@${CLI_VERSION}`;
25
- }
26
- function shellQuote(value) {
27
- return `'${value.replace(/'/g, "'\\''")}'`;
28
- }
29
- function runCommand(args, extraEnv = {}) {
30
- const timeoutMs = Math.max(MIN_TIMEOUT_SECONDS, Number(args[0]) || MIN_TIMEOUT_SECONDS) * 1000;
31
- const commandArgs = args.slice(1);
32
- return new Promise((resolve) => {
33
- const child = spawn(process.execPath, [process.argv[1] ?? "", ...commandArgs], {
34
- env: {
35
- ...process.env,
36
- ...extraEnv,
37
- NO_UPDATE_NOTIFIER: "1",
38
- npm_config_update_notifier: "false",
39
- },
40
- stdio: ["ignore", "pipe", "pipe"],
41
- });
42
- let stdout = "";
43
- let stderr = "";
44
- let timedOut = false;
45
- const timer = setTimeout(() => {
46
- timedOut = true;
47
- child.kill("SIGTERM");
48
- setTimeout(() => child.kill("SIGKILL"), 5000).unref();
49
- }, timeoutMs);
50
- child.stdout.setEncoding("utf8");
51
- child.stderr.setEncoding("utf8");
52
- child.stdout.on("data", (chunk) => { stdout += chunk; });
53
- child.stderr.on("data", (chunk) => { stderr += chunk; });
54
- child.on("close", (code) => {
55
- clearTimeout(timer);
56
- resolve({ code: code ?? 1, stdout, stderr, timedOut });
57
- });
58
- });
59
- }
60
- async function appendLog(message) {
61
- await mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
62
- await appendFile(LOG_PATH, message, { mode: 0o600 });
63
- }
64
- async function writeStatus(status) {
65
- await mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
66
- await writeFile(STATUS_PATH, `${JSON.stringify(status, null, 2)}\n`, { mode: 0o600 });
67
- }
68
- async function readStatus() {
69
- try {
70
- return JSON.parse(await readFile(STATUS_PATH, "utf8"));
71
- }
72
- catch (err) {
73
- if (err.code === "ENOENT")
74
- return null;
75
- throw err;
76
- }
77
- }
78
- function pidIsAlive(pid) {
79
- if (!pid || pid < 1)
80
- return false;
81
- try {
82
- process.kill(pid, 0);
83
- return true;
84
- }
85
- catch {
86
- return false;
87
- }
88
- }
89
- function statusIsLive(status) {
90
- if (!status?.running)
91
- return false;
92
- if (!pidIsAlive(status.pid))
93
- return false;
94
- if (!status.next_run_at || !status.interval_seconds)
95
- return true;
96
- const nextRun = Date.parse(status.next_run_at);
97
- if (!Number.isFinite(nextRun))
98
- return false;
99
- const graceMs = Math.max(status.interval_seconds * 2000, 120_000);
100
- return nextRun + graceMs >= Date.now();
101
- }
102
- function parseSyncResult(output) {
103
- const match = output.match(/synced\s+(\d+)\s+skills\s+\((\d+)\s+unchanged,\s+(\d+)\s+updated(?:,\s+(\d+)\s+conflicts?\s+skipped)?/i);
104
- if (!match)
105
- return {};
106
- return {
107
- synced: Number(match[1]),
108
- updated: Number(match[3]),
109
- conflicts: Number(match[4] ?? "0"),
110
- };
111
- }
112
- async function runTarget(target, opts) {
113
- const started = Date.now();
114
- const cacheDir = join(CONFIG_DIR, "skill-cache", target);
115
- const nativeManifestPath = join(CONFIG_DIR, "native-sync-manifests", `${target}.json`);
116
- const syncResult = await runCommand([String(opts.timeoutSeconds), "sync", "--target", target], {
117
- [targetSkillsDirEnv(target)]: cacheDir,
118
- FLOOM_SYNC_MANIFEST_PATH: join(cacheDir, ".floom-cli-sync-manifest.json"),
119
- });
120
- const combined = `${syncResult.stdout}\n${syncResult.stderr}`;
121
- let ok = syncResult.code === 0 && !syncResult.timedOut;
122
- let error = syncResult.timedOut ? "sync timed out" : syncResult.code === 0 ? undefined : combined.trim() || "sync failed";
123
- if (ok) {
124
- const setupResult = await runCommand([String(opts.timeoutSeconds), "setup", "--target", target, "--yes", "--global"]);
125
- if (setupResult.code !== 0 || setupResult.timedOut) {
126
- ok = false;
127
- error = setupResult.timedOut ? "instruction setup timed out" : `${setupResult.stdout}\n${setupResult.stderr}`.trim() || "instruction setup failed";
128
- }
129
- }
130
- if (opts.push && ok) {
131
- if (opts.yolo && !(await fileExists(nativeBaselinePath(target)))) {
132
- const baselineResult = await runCommand([String(opts.timeoutSeconds), "watch", "--push", "--once", "--target", target, "--no-yolo"], { FLOOM_SYNC_MANIFEST_PATH: nativeManifestPath, FLOOM_BASELINE_ADOPT_ALL: "1" });
133
- if (baselineResult.code !== 0 || baselineResult.timedOut) {
134
- ok = false;
135
- error = baselineResult.timedOut ? "native baseline timed out" : `${baselineResult.stdout}\n${baselineResult.stderr}`.trim() || "native baseline failed";
136
- }
137
- else {
138
- await writeNativeBaselineMarker(target);
139
- }
140
- }
141
- }
142
- if (opts.push && ok) {
143
- const pushArgs = ["watch", "--push", "--once", "--target", target];
144
- if (!opts.yolo)
145
- pushArgs.push("--no-yolo");
146
- const pushResult = await runCommand([String(opts.timeoutSeconds), ...pushArgs], { FLOOM_SYNC_MANIFEST_PATH: nativeManifestPath });
147
- if (pushResult.code !== 0 || pushResult.timedOut) {
148
- ok = false;
149
- error = pushResult.timedOut ? "push timed out" : `${pushResult.stdout}\n${pushResult.stderr}`.trim() || "push failed";
150
- }
151
- }
152
- await appendLog([
153
- `${new Date().toISOString()} target=${target} ok=${ok}`,
154
- combined.trim(),
155
- error && !combined.includes(error) ? error : "",
156
- "",
157
- ].filter(Boolean).join("\n"));
158
- return {
159
- ok,
160
- ...parseSyncResult(combined),
161
- ...(error ? { error } : {}),
162
- duration_ms: Date.now() - started,
163
- };
164
- }
165
- async function fileExists(path) {
166
- try {
167
- await access(path);
168
- return true;
169
- }
170
- catch (err) {
171
- if (err.code === "ENOENT")
172
- return false;
173
- throw err;
174
- }
175
- }
176
- function nativeBaselinePath(target) {
177
- return join(CONFIG_DIR, "native-sync-manifests", `${target}.baseline-v${NATIVE_BASELINE_VERSION}.json`);
178
- }
179
- async function writeNativeBaselineMarker(target) {
180
- const path = nativeBaselinePath(target);
181
- await mkdir(dirname(path), { recursive: true, mode: 0o700 });
182
- await writeFile(path, `${JSON.stringify({
183
- version: NATIVE_BASELINE_VERSION,
184
- target,
185
- created_at: new Date().toISOString(),
186
- }, null, 2)}\n`, { mode: 0o600 });
187
- }
188
- async function sleep(ms) {
189
- await new Promise((resolve) => setTimeout(resolve, ms));
190
- }
191
- export async function runDaemonForeground(opts) {
192
- const targets = targetsFor(opts.target);
193
- process.stdout.write(`${symbols.bullet} Floom daemon running for ${targets.join(", ")} every ${opts.intervalSeconds}s.\n`);
194
- for (;;) {
195
- const now = new Date();
196
- const status = {
197
- running: true,
198
- manager: "foreground",
199
- service: "foreground",
200
- pid: process.pid,
201
- version: CLI_VERSION,
202
- hostname: hostname(),
203
- interval_seconds: opts.intervalSeconds,
204
- timeout_seconds: opts.timeoutSeconds,
205
- targets,
206
- push: opts.push,
207
- yolo: opts.yolo,
208
- last_started_at: now.toISOString(),
209
- last_run: {},
210
- };
211
- await writeStatus(status);
212
- for (const target of targets) {
213
- status.last_run[target] = await runTarget(target, opts);
214
- await writeStatus(status);
215
- }
216
- const completed = new Date();
217
- status.last_completed_at = completed.toISOString();
218
- status.next_run_at = new Date(completed.getTime() + opts.intervalSeconds * 1000).toISOString();
219
- await writeStatus(status);
220
- await appendLog(`${completed.toISOString()} daemon cycle complete\n`);
221
- if (!opts.foreground)
222
- return;
223
- await sleep(opts.intervalSeconds * 1000);
224
- }
225
- }
226
- function serviceCommand(opts) {
227
- const args = [
228
- "daemon",
229
- "run",
230
- "--foreground",
231
- "--target",
232
- opts.target,
233
- "--interval",
234
- String(opts.intervalSeconds),
235
- "--timeout",
236
- String(opts.timeoutSeconds),
237
- opts.push ? "--push" : "--no-push",
238
- opts.yolo ? "--yolo" : "--no-yolo",
239
- ];
240
- return `exec ${cliInvocation()} ${args.map(shellQuote).join(" ")}`;
241
- }
242
- function systemdUnit(opts, user) {
243
- return `[Unit]
244
- Description=Floom always-on skill sync
245
- After=network-online.target
246
- Wants=network-online.target
247
-
248
- [Service]
249
- Type=simple
250
- ExecStart=/bin/sh -lc ${shellQuote(serviceCommand(opts))}
251
- Restart=always
252
- RestartSec=15
253
- Environment=NO_UPDATE_NOTIFIER=1
254
- Environment=npm_config_update_notifier=false
255
-
256
- [Install]
257
- WantedBy=${user ? "default.target" : "multi-user.target"}
258
- `;
259
- }
260
- function launchdPlist(opts) {
261
- const command = serviceCommand(opts);
262
- return `<?xml version="1.0" encoding="UTF-8"?>
263
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
264
- <plist version="1.0">
265
- <dict>
266
- <key>Label</key>
267
- <string>${LAUNCHD_LABEL}</string>
268
- <key>ProgramArguments</key>
269
- <array>
270
- <string>/bin/sh</string>
271
- <string>-lc</string>
272
- <string>${escapePlist(command)}</string>
273
- </array>
274
- <key>RunAtLoad</key>
275
- <true/>
276
- <key>KeepAlive</key>
277
- <true/>
278
- <key>StandardOutPath</key>
279
- <string>${LOG_PATH}</string>
280
- <key>StandardErrorPath</key>
281
- <string>${LOG_PATH}</string>
282
- </dict>
283
- </plist>
284
- `;
285
- }
286
- function escapePlist(value) {
287
- return value
288
- .replace(/&/g, "&amp;")
289
- .replace(/</g, "&lt;")
290
- .replace(/>/g, "&gt;");
291
- }
292
- function manager() {
293
- return platform() === "darwin" ? "launchd" : "systemd";
294
- }
295
- function systemdPath() {
296
- const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
297
- if (isRoot)
298
- return { path: `/etc/systemd/system/${SERVICE_NAME}`, user: false };
299
- return { path: join(homedir(), ".config", "systemd", "user", SERVICE_NAME), user: true };
300
- }
301
- async function installDaemon(opts) {
302
- if (manager() === "launchd") {
303
- const plistPath = join(homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
304
- const body = launchdPlist(opts);
305
- if (opts.dryRun) {
306
- process.stdout.write(body);
307
- return;
308
- }
309
- await mkdir(dirname(plistPath), { recursive: true });
310
- await writeFile(plistPath, body, "utf8");
311
- process.stdout.write(`${symbols.ok} Installed ${plistPath}\n`);
312
- await runManagerCommand("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? ""}`, plistPath], true);
313
- await runManagerCommand("launchctl", ["kickstart", "-k", `gui/${process.getuid?.() ?? ""}/${LAUNCHD_LABEL}`], true);
314
- return;
315
- }
316
- const target = systemdPath();
317
- const body = systemdUnit(opts, target.user);
318
- if (opts.dryRun) {
319
- process.stdout.write(body);
320
- return;
321
- }
322
- await mkdir(dirname(target.path), { recursive: true });
323
- await writeFile(target.path, body, "utf8");
324
- process.stdout.write(`${symbols.ok} Installed ${target.path}\n`);
325
- await runSystemctl(["daemon-reload"], target.user, true);
326
- await runSystemctl(["enable", "--now", SERVICE_NAME], target.user, true);
327
- }
328
- async function showStatus(opts) {
329
- const status = await readStatus();
330
- const live = statusIsLive(status);
331
- if (opts.json) {
332
- process.stdout.write(`${JSON.stringify(status ? { ...status, running: live } : { running: false }, null, 2)}\n`);
333
- return;
334
- }
335
- if (!status) {
336
- process.stdout.write(`${symbols.bullet} Floom daemon has no status yet.\n`);
337
- return;
338
- }
339
- process.stdout.write(`${live ? symbols.ok : symbols.bullet} Floom daemon ${live ? "status" : "last status"}\n`);
340
- process.stdout.write(` ${c.dim("manager:")} ${status.manager}\n`);
341
- process.stdout.write(` ${c.dim("targets:")} ${status.targets.join(", ")}\n`);
342
- if (status.last_completed_at)
343
- process.stdout.write(` ${c.dim("last completed:")} ${status.last_completed_at}\n`);
344
- for (const [target, run] of Object.entries(status.last_run)) {
345
- process.stdout.write(` ${run.ok ? symbols.ok : symbols.fail} ${target}: ${run.ok ? "ok" : run.error ?? "failed"}\n`);
346
- }
347
- }
348
- async function showLogs(opts) {
349
- const body = await readFile(LOG_PATH, "utf8").catch((err) => {
350
- if (err.code === "ENOENT")
351
- return "";
352
- throw err;
353
- });
354
- const lines = body.split(/\r?\n/);
355
- process.stdout.write(`${lines.slice(Math.max(0, lines.length - opts.tail)).join("\n")}\n`);
356
- }
357
- function execManager(command, args) {
358
- return new Promise((resolve) => {
359
- const child = spawn(command, args, { stdio: ["ignore", "ignore", "pipe"] });
360
- let stderr = "";
361
- child.stderr.setEncoding("utf8");
362
- child.stderr.on("data", (chunk) => { stderr += chunk; });
363
- child.on("error", (err) => resolve({ code: 127, stderr: err.message }));
364
- child.on("close", (code) => resolve({ code: code ?? 1, stderr }));
365
- });
366
- }
367
- async function runManagerCommand(command, args, soft) {
368
- const result = await execManager(command, args);
369
- if (result.code === 0)
370
- return;
371
- const rendered = `${command} ${args.join(" ")}`;
372
- if (soft) {
373
- process.stdout.write(` ${c.dim(`Run manually if needed: ${rendered}`)}\n`);
374
- return;
375
- }
376
- throw new FloomError(`Failed to run ${rendered}.`, result.stderr.trim() || "Service manager command failed.");
377
- }
378
- async function runSystemctl(args, user, soft) {
379
- await runManagerCommand("systemctl", user ? ["--user", ...args] : args, soft);
380
- }
381
- async function restartDaemon() {
382
- if (manager() === "launchd") {
383
- await runManagerCommand("launchctl", ["kickstart", "-k", `gui/${process.getuid?.() ?? ""}/${LAUNCHD_LABEL}`], false);
384
- process.stdout.write(`${symbols.ok} Restarted ${LAUNCHD_LABEL}\n`);
385
- return;
386
- }
387
- const target = systemdPath();
388
- await runSystemctl(["restart", SERVICE_NAME], target.user, false);
389
- process.stdout.write(`${symbols.ok} Restarted ${SERVICE_NAME}\n`);
390
- }
391
- async function uninstallDaemon() {
392
- if (manager() === "launchd") {
393
- const plistPath = join(homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
394
- await runManagerCommand("launchctl", ["bootout", `gui/${process.getuid?.() ?? ""}/${LAUNCHD_LABEL}`], true);
395
- await unlink(plistPath).catch((err) => {
396
- if (err.code !== "ENOENT")
397
- throw err;
398
- });
399
- process.stdout.write(`${symbols.ok} Uninstalled ${LAUNCHD_LABEL}\n`);
400
- return;
401
- }
402
- const target = systemdPath();
403
- await runSystemctl(["disable", "--now", SERVICE_NAME], target.user, true);
404
- await unlink(target.path).catch((err) => {
405
- if (err.code !== "ENOENT")
406
- throw err;
407
- });
408
- await runSystemctl(["daemon-reload"], target.user, true);
409
- process.stdout.write(`${symbols.ok} Uninstalled ${SERVICE_NAME}\n`);
410
- }
411
- export async function daemon(opts) {
412
- switch (opts.command) {
413
- case "install":
414
- await installDaemon(opts);
415
- return;
416
- case "status":
417
- await showStatus(opts);
418
- return;
419
- case "logs":
420
- await showLogs(opts);
421
- return;
422
- case "run":
423
- await runDaemonForeground(opts);
424
- return;
425
- case "restart":
426
- await restartDaemon();
427
- return;
428
- case "uninstall":
429
- await uninstallDaemon();
430
- return;
431
- default:
432
- throw new FloomError("Missing daemon command.", "Use install, status, logs, restart, uninstall, or run.");
433
- }
434
- }
435
- export function normalizeDaemonOptions(opts) {
436
- if (opts.intervalSeconds < MIN_INTERVAL_SECONDS) {
437
- throw new FloomError("Invalid --interval.", `Use an integer number of seconds, minimum ${MIN_INTERVAL_SECONDS}.`);
438
- }
439
- if (opts.timeoutSeconds < MIN_TIMEOUT_SECONDS) {
440
- throw new FloomError("Invalid --timeout.", `Use an integer number of seconds, minimum ${MIN_TIMEOUT_SECONDS}.`);
441
- }
442
- return opts;
443
- }
444
- export function parseDaemonTarget(value) {
445
- if (value === "all")
446
- return "all";
447
- if (isAgentTarget(value))
448
- return value;
449
- throw new FloomError("Invalid --target.", "Use claude|codex|cursor|opencode|kimi|all.");
450
- }
package/dist/delete.js DELETED
@@ -1,55 +0,0 @@
1
- import { createInterface } from "node:readline/promises";
2
- import { stdin as input, stdout as output } from "node:process";
3
- import ora from "ora";
4
- import { readConfig, resolveApiUrl } from "./config.js";
5
- import { deleteRequest } from "./lib/api.js";
6
- import { c, symbols } from "./ui.js";
7
- import { FloomError } from "./errors.js";
8
- function slugFromInput(s) {
9
- const trimmed = s.trim();
10
- try {
11
- const url = new URL(trimmed);
12
- return (url.pathname.split("/").filter(Boolean).at(-1) ?? "").replace(/\.(md|json)$/i, "");
13
- }
14
- catch {
15
- return trimmed.replace(/\.(md|json)$/i, "");
16
- }
17
- }
18
- async function confirm(question) {
19
- if (!process.stdin.isTTY) {
20
- throw new FloomError("Refusing to delete without confirmation in non-interactive mode.", "Pass `--yes` to skip the prompt.");
21
- }
22
- const rl = createInterface({ input, output });
23
- try {
24
- const answer = (await rl.question(` ${question} (y/N) `)).trim().toLowerCase();
25
- return answer === "y" || answer === "yes";
26
- }
27
- finally {
28
- rl.close();
29
- }
30
- }
31
- export async function deleteSkill(opts) {
32
- const slug = slugFromInput(opts.slug);
33
- if (!slug)
34
- throw new FloomError("Missing skill slug.", "Try: `npx -y @floomhq/floom delete <slug>`");
35
- const cfg = await readConfig();
36
- if (!cfg)
37
- throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
38
- if (!opts.yes) {
39
- process.stdout.write(`\n${symbols.bullet} About to delete ${c.bold(slug)}.\n`);
40
- const ok = await confirm(`Delete ${c.bold(slug)}? Cannot be undone in CLI.`);
41
- if (!ok) {
42
- process.stdout.write(`\n${c.dim("Cancelled.")}\n\n`);
43
- return;
44
- }
45
- }
46
- const apiUrl = resolveApiUrl(cfg);
47
- const spinner = ora({ text: c.dim(`Deleting ${slug}...`), color: "yellow" }).start();
48
- try {
49
- await deleteRequest(`${apiUrl}/api/v1/skills/${encodeURIComponent(slug)}`, "delete skill", cfg.accessToken);
50
- }
51
- finally {
52
- spinner.stop();
53
- }
54
- process.stdout.write(`\n${symbols.ok} Deleted ${c.bold(slug)}\n\n`);
55
- }