@floomhq/floom 1.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/LICENSE +21 -0
- package/README.md +53 -0
- package/bin/floom.js +2 -0
- package/dist/cli.js +355 -0
- package/dist/config.js +44 -0
- package/dist/delete.js +55 -0
- package/dist/doctor.js +270 -0
- package/dist/errors.js +51 -0
- package/dist/info.js +66 -0
- package/dist/init.js +59 -0
- package/dist/install.js +175 -0
- package/dist/lib/api.js +58 -0
- package/dist/library.js +77 -0
- package/dist/list.js +60 -0
- package/dist/login.js +163 -0
- package/dist/mcp.js +22 -0
- package/dist/publish.js +189 -0
- package/dist/share.js +70 -0
- package/dist/sync-manifest.js +123 -0
- package/dist/sync.js +402 -0
- package/dist/ui.js +31 -0
- package/dist/whoami.js +61 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Floom
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# @floomhq/floom
|
|
2
|
+
|
|
3
|
+
Publish AI skills from your terminal. Share them with a link. Add other people's skills with one command.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install -g @floomhq/floom
|
|
7
|
+
floom login
|
|
8
|
+
floom publish my-skill.md
|
|
9
|
+
floom add awesome-skill
|
|
10
|
+
floom list
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Returns a shareable link like `https://floom.dev/s/ffas93ud`. Anyone with the URL can read the raw Markdown — drop it into any AI tool that accepts skills.
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
- `floom login` — sign in with Google. New accounts are created on first login. Token stored at `~/.floom/config.json`.
|
|
18
|
+
- `floom init [file.md]` — create a starter skill file.
|
|
19
|
+
- `floom publish <file.md>` — upload a Markdown file. Optional `--public` / `--private` / `--unlisted`, `--type knowledge|instruction|workflow|skill`, `--installs-as <target>`, and `--version <label>`.
|
|
20
|
+
- `floom list` — show your published skills. Optional `--json`.
|
|
21
|
+
- `floom add <url-or-slug>` — fetch a skill into `~/.claude/skills/<slug>.md`.
|
|
22
|
+
- `floom info <url-or-slug>` — show skill metadata. Optional `--json`.
|
|
23
|
+
- `floom sync` — preview: pull your own Floom-published skills into `~/.claude/skills/`.
|
|
24
|
+
- `floom watch` — preview: run `floom sync` repeatedly. Optional `--interval <seconds>`; minimum `10`.
|
|
25
|
+
- `floom delete <url-or-slug>` — delete one of your published skills. Optional `--yes`.
|
|
26
|
+
- `floom doctor` — diagnose your Floom setup.
|
|
27
|
+
- `floom whoami` — show the signed-in account.
|
|
28
|
+
- `floom logout` — delete local credentials.
|
|
29
|
+
|
|
30
|
+
## Skill format
|
|
31
|
+
|
|
32
|
+
Optional YAML-ish frontmatter (`title`, `description`, `version`), then freeform Markdown.
|
|
33
|
+
|
|
34
|
+
```markdown
|
|
35
|
+
---
|
|
36
|
+
title: Write a LinkedIn post
|
|
37
|
+
description: Yurii-level value density
|
|
38
|
+
version: 0.1.0
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
# Instructions
|
|
42
|
+
- ...
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Configuration
|
|
46
|
+
|
|
47
|
+
Override the API host with `FLOOM_API_URL` (defaults to `https://floom.dev`).
|
|
48
|
+
|
|
49
|
+
`floom sync` and `floom watch` are Version 1 preview commands for your own published skills. They store a machine-local manifest at `~/.floom/sync-manifest.json`.
|
|
50
|
+
The manifest records hashes for files Floom previously wrote. Version 1 sync writes missing files
|
|
51
|
+
only. Remote updates, existing untracked files, and locally edited tracked files are skipped as
|
|
52
|
+
conflicts. Symlinks are never followed. To accept the Floom version, move or delete the local file
|
|
53
|
+
and run `floom sync` again.
|
package/bin/floom.js
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import updateNotifier from "update-notifier";
|
|
3
|
+
import { login } from "./login.js";
|
|
4
|
+
import { publish } from "./publish.js";
|
|
5
|
+
import { whoami } from "./whoami.js";
|
|
6
|
+
import { init } from "./init.js";
|
|
7
|
+
import { deleteConfig } from "./config.js";
|
|
8
|
+
import { list } from "./list.js";
|
|
9
|
+
import { install } from "./install.js";
|
|
10
|
+
import { info } from "./info.js";
|
|
11
|
+
import { deleteSkill } from "./delete.js";
|
|
12
|
+
import { doctor } from "./doctor.js";
|
|
13
|
+
import { sync } from "./sync.js";
|
|
14
|
+
import { c, symbols } from "./ui.js";
|
|
15
|
+
import { printError, FloomError } from "./errors.js";
|
|
16
|
+
const VERSION = "1.0.0";
|
|
17
|
+
const PKG = { name: "@floomhq/floom", version: VERSION };
|
|
18
|
+
const V1_NOT_AVAILABLE = "Not available in Floom Version 1.";
|
|
19
|
+
function usage() {
|
|
20
|
+
const out = `
|
|
21
|
+
${c.coral(" ____ __")}
|
|
22
|
+
${c.coral(" / __ \\___ / /___ ___ __")}
|
|
23
|
+
${c.coral(" / /_/ / _ \\/ / __ `/ / / /")}
|
|
24
|
+
${c.coral(" / _, _/ __/ / /_/ / /_/ /")}
|
|
25
|
+
${c.coral(" /_/ |_|\\___/_/\\__,_/\\__, /")} ${c.dim(`v${VERSION}`)}
|
|
26
|
+
${c.coral(" /____/")}
|
|
27
|
+
|
|
28
|
+
${c.bold("Share AI agent skills with a link.")}
|
|
29
|
+
${c.dim("Publish knowledge, instructions, and workflows from your terminal.")}
|
|
30
|
+
|
|
31
|
+
${c.bold("Usage")}
|
|
32
|
+
${c.cyan("floom")} ${c.dim("<command> [options]")}
|
|
33
|
+
|
|
34
|
+
${c.bold("Start Here")}
|
|
35
|
+
${c.cyan("floom add")} ${c.dim("https://floom.dev/s/ffas93ud")}
|
|
36
|
+
${c.dim("Install a public skill. No account needed.")}
|
|
37
|
+
|
|
38
|
+
${c.cyan("floom publish")} ${c.dim("./support-tone.md --type instruction --public")}
|
|
39
|
+
${c.dim("Publish Markdown and print a share link.")}
|
|
40
|
+
|
|
41
|
+
${c.bold("Commands")}
|
|
42
|
+
${c.dim("Receive")}
|
|
43
|
+
${c.cyan("add")} ${c.dim("<url-or-slug>")} Install into ~/.claude/skills/
|
|
44
|
+
${c.cyan("info")} ${c.dim("<url-or-slug>")} Show metadata ${c.dim("[--json]")}
|
|
45
|
+
|
|
46
|
+
${c.dim("Create")}
|
|
47
|
+
${c.cyan("init")} ${c.dim("[file.md]")} Create a starter skill file
|
|
48
|
+
${c.cyan("publish")} ${c.dim("<file.md>")} Upload Markdown and print a URL
|
|
49
|
+
|
|
50
|
+
${c.dim("Publish Flags")}
|
|
51
|
+
${c.dim("--public | --private | --unlisted")} ${c.dim("Set link visibility")}
|
|
52
|
+
${c.dim("--type <kind>")} ${c.dim("knowledge | instruction | workflow | skill")}
|
|
53
|
+
${c.dim("--installs-as <target>")} ${c.dim("Set install target metadata")}
|
|
54
|
+
${c.dim("claude_skill | memory | rule | codex_instruction")}
|
|
55
|
+
${c.dim("--version <label>")} ${c.dim("Attach a human version label")}
|
|
56
|
+
|
|
57
|
+
${c.dim("Account")}
|
|
58
|
+
${c.cyan("login")} Sign in with Google
|
|
59
|
+
${c.cyan("list")} List your published skills ${c.dim("[--json]")}
|
|
60
|
+
${c.cyan("delete")} ${c.dim("<url-or-slug>")} Delete one of your skills ${c.dim("[--yes]")}
|
|
61
|
+
${c.cyan("whoami")} Show the signed-in account
|
|
62
|
+
${c.cyan("logout")} Delete local credentials
|
|
63
|
+
|
|
64
|
+
${c.dim("System")}
|
|
65
|
+
${c.cyan("sync")} Preview: pull your own published skills
|
|
66
|
+
${c.cyan("watch")} Preview: poll your own published skills ${c.dim("[--interval <seconds>, min 10]")}
|
|
67
|
+
${c.cyan("doctor")} Diagnose auth, API, and local setup
|
|
68
|
+
${c.cyan("--help")} Show this help
|
|
69
|
+
${c.cyan("--version")} Show version
|
|
70
|
+
|
|
71
|
+
${c.bold("Env")}
|
|
72
|
+
${c.cyan("FLOOM_API_URL")} Override the API host
|
|
73
|
+
|
|
74
|
+
${c.bold("Links")}
|
|
75
|
+
${c.dim("Docs")} https://floom.dev
|
|
76
|
+
${c.dim("Source")} https://github.com/floomhq/floom
|
|
77
|
+
`;
|
|
78
|
+
process.stdout.write(out);
|
|
79
|
+
}
|
|
80
|
+
const ASSET_TYPES = new Set(["knowledge", "instruction", "workflow", "skill"]);
|
|
81
|
+
const INSTALL_TARGETS = new Set([
|
|
82
|
+
"claude_skill",
|
|
83
|
+
"memory",
|
|
84
|
+
"rule",
|
|
85
|
+
"codex_instruction",
|
|
86
|
+
"opencode_instruction",
|
|
87
|
+
"cursor_rule",
|
|
88
|
+
"other",
|
|
89
|
+
]);
|
|
90
|
+
const VERSION_RE = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$/;
|
|
91
|
+
function readFlagValue(argv, index, flag) {
|
|
92
|
+
const current = argv[index] ?? "";
|
|
93
|
+
if (current.startsWith(`${flag}=`))
|
|
94
|
+
return { value: current.slice(flag.length + 1), nextIndex: index };
|
|
95
|
+
const value = argv[index + 1];
|
|
96
|
+
if (!value || value.startsWith("--"))
|
|
97
|
+
throw new FloomError(`Missing value for ${flag}.`);
|
|
98
|
+
return { value, nextIndex: index + 1 };
|
|
99
|
+
}
|
|
100
|
+
function parseFlags(argv) {
|
|
101
|
+
const out = { visibility: "unlisted", update: false, rest: [] };
|
|
102
|
+
for (let i = 0; i < argv.length; i++) {
|
|
103
|
+
const a = argv[i] ?? "";
|
|
104
|
+
if (a === "--public")
|
|
105
|
+
out.visibility = "public";
|
|
106
|
+
else if (a === "--private")
|
|
107
|
+
out.visibility = "private";
|
|
108
|
+
else if (a === "--unlisted")
|
|
109
|
+
out.visibility = "unlisted";
|
|
110
|
+
else if (a === "--update") {
|
|
111
|
+
throw new FloomError(V1_NOT_AVAILABLE, "`floom publish --update` is planned for a later Floom release.");
|
|
112
|
+
}
|
|
113
|
+
else if (a === "--share" || a.startsWith("--share=")) {
|
|
114
|
+
throw new FloomError(V1_NOT_AVAILABLE, "`floom publish --share` is planned for a later Floom release.");
|
|
115
|
+
}
|
|
116
|
+
else if (a === "--type" || a.startsWith("--type=")) {
|
|
117
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--type");
|
|
118
|
+
if (!ASSET_TYPES.has(value)) {
|
|
119
|
+
throw new FloomError(`Invalid --type: ${value}`, "Use one of: knowledge, instruction, workflow, skill.");
|
|
120
|
+
}
|
|
121
|
+
out.assetType = value;
|
|
122
|
+
i = nextIndex;
|
|
123
|
+
}
|
|
124
|
+
else if (a === "--installs-as" || a.startsWith("--installs-as=")) {
|
|
125
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--installs-as");
|
|
126
|
+
if (!INSTALL_TARGETS.has(value)) {
|
|
127
|
+
throw new FloomError(`Invalid --installs-as: ${value}`, "Use one of: claude_skill, memory, rule, codex_instruction, opencode_instruction, cursor_rule, other.");
|
|
128
|
+
}
|
|
129
|
+
out.installsAs = value;
|
|
130
|
+
i = nextIndex;
|
|
131
|
+
}
|
|
132
|
+
else if (a === "--version" || a.startsWith("--version=")) {
|
|
133
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--version");
|
|
134
|
+
if (!VERSION_RE.test(value)) {
|
|
135
|
+
throw new FloomError(`Invalid --version: ${value}`, "Use 1-64 characters: letters, numbers, dots, underscores, plus, or hyphen.");
|
|
136
|
+
}
|
|
137
|
+
out.version = value;
|
|
138
|
+
i = nextIndex;
|
|
139
|
+
}
|
|
140
|
+
else
|
|
141
|
+
out.rest.push(a);
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
function parseListFlags(argv) {
|
|
146
|
+
const out = { json: false };
|
|
147
|
+
for (const a of argv) {
|
|
148
|
+
if (a === "--json")
|
|
149
|
+
out.json = true;
|
|
150
|
+
else if (a.startsWith("--")) {
|
|
151
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `floom list --help` for usage.");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
function parseInfoFlags(argv) {
|
|
157
|
+
const out = { json: false };
|
|
158
|
+
for (const a of argv) {
|
|
159
|
+
if (a === "--json")
|
|
160
|
+
out.json = true;
|
|
161
|
+
else if (a.startsWith("--"))
|
|
162
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `floom info <slug> --json`.");
|
|
163
|
+
else if (!out.slug)
|
|
164
|
+
out.slug = a;
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
function parseDeleteFlags(argv) {
|
|
169
|
+
const out = { yes: false };
|
|
170
|
+
for (const a of argv) {
|
|
171
|
+
if (a === "--yes" || a === "-y")
|
|
172
|
+
out.yes = true;
|
|
173
|
+
else if (a.startsWith("--"))
|
|
174
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `floom delete <slug> --yes`.");
|
|
175
|
+
else if (!out.slug)
|
|
176
|
+
out.slug = a;
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
function parseWatchFlags(argv) {
|
|
181
|
+
const out = { intervalSeconds: 60 };
|
|
182
|
+
for (let i = 0; i < argv.length; i++) {
|
|
183
|
+
const a = argv[i] ?? "";
|
|
184
|
+
if (a === "--interval" || a.startsWith("--interval=")) {
|
|
185
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--interval");
|
|
186
|
+
const interval = Number(value);
|
|
187
|
+
if (!Number.isInteger(interval) || interval < 10) {
|
|
188
|
+
throw new FloomError("Invalid --interval.", "Use an integer number of seconds, minimum 10.");
|
|
189
|
+
}
|
|
190
|
+
out.intervalSeconds = interval;
|
|
191
|
+
i = nextIndex;
|
|
192
|
+
}
|
|
193
|
+
else if (a.startsWith("--")) {
|
|
194
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `floom watch --interval 60`.");
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom watch --interval 60`.");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
function notAvailable(feature) {
|
|
203
|
+
throw new FloomError(V1_NOT_AVAILABLE, `${feature} is planned for a later Floom release.`);
|
|
204
|
+
}
|
|
205
|
+
function sleep(ms, signal) {
|
|
206
|
+
if (signal.aborted)
|
|
207
|
+
return Promise.resolve();
|
|
208
|
+
return new Promise((resolve) => {
|
|
209
|
+
const timer = setTimeout(resolve, ms);
|
|
210
|
+
signal.addEventListener("abort", () => {
|
|
211
|
+
clearTimeout(timer);
|
|
212
|
+
resolve();
|
|
213
|
+
}, { once: true });
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
async function watch(intervalSeconds) {
|
|
217
|
+
const controller = new AbortController();
|
|
218
|
+
let stopping = false;
|
|
219
|
+
const stop = () => {
|
|
220
|
+
if (stopping)
|
|
221
|
+
return;
|
|
222
|
+
stopping = true;
|
|
223
|
+
controller.abort();
|
|
224
|
+
process.stdout.write(`\n${symbols.bullet} Stopping floom watch\n`);
|
|
225
|
+
process.exit(0);
|
|
226
|
+
};
|
|
227
|
+
process.on("SIGINT", stop);
|
|
228
|
+
process.on("SIGTERM", stop);
|
|
229
|
+
process.stdout.write(`${symbols.bullet} Watching Floom sync every ${intervalSeconds}s. Press Ctrl-C to stop.\n`);
|
|
230
|
+
while (!controller.signal.aborted) {
|
|
231
|
+
await sync({ spinner: false, quietUnchanged: true });
|
|
232
|
+
await sleep(intervalSeconds * 1000, controller.signal);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function main() {
|
|
236
|
+
const [, , cmd, ...rest] = process.argv;
|
|
237
|
+
// Update notifier — runs in background, prints at process exit if a newer
|
|
238
|
+
// version is available. Disabled in CI / non-TTY by default.
|
|
239
|
+
if (cmd !== "watch") {
|
|
240
|
+
try {
|
|
241
|
+
updateNotifier({ pkg: PKG, updateCheckInterval: 1000 * 60 * 60 * 24 }).notify({
|
|
242
|
+
defer: true,
|
|
243
|
+
isGlobal: true,
|
|
244
|
+
message: `${symbols.bullet} ${c.bold("floom")} v{latestVersion} available — run \`npm i -g {packageName}\` to update.`,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// never block on update-notifier
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Subcommand --help: any rest arg = --help/-h/help → show top-level usage.
|
|
252
|
+
// Subcommands are simple enough that one help screen is fine for Version 1.
|
|
253
|
+
if (rest.includes("--help") || rest.includes("-h") || rest.includes("help")) {
|
|
254
|
+
usage();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
switch (cmd) {
|
|
259
|
+
case undefined:
|
|
260
|
+
case "--help":
|
|
261
|
+
case "-h":
|
|
262
|
+
case "help":
|
|
263
|
+
usage();
|
|
264
|
+
return;
|
|
265
|
+
case "--version":
|
|
266
|
+
case "-v":
|
|
267
|
+
process.stdout.write(`${VERSION}\n`);
|
|
268
|
+
return;
|
|
269
|
+
case "login":
|
|
270
|
+
await login();
|
|
271
|
+
return;
|
|
272
|
+
case "logout":
|
|
273
|
+
await deleteConfig();
|
|
274
|
+
process.stdout.write(`\n${symbols.ok} Signed out\n\n`);
|
|
275
|
+
return;
|
|
276
|
+
case "whoami":
|
|
277
|
+
await whoami();
|
|
278
|
+
return;
|
|
279
|
+
case "init": {
|
|
280
|
+
const file = rest[0];
|
|
281
|
+
await init(file);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
case "publish": {
|
|
285
|
+
const flags = parseFlags(rest);
|
|
286
|
+
const file = flags.rest[0];
|
|
287
|
+
if (!file) {
|
|
288
|
+
throw new FloomError("Missing file argument.", "Try: `floom publish skill.md`");
|
|
289
|
+
}
|
|
290
|
+
await publish({
|
|
291
|
+
file,
|
|
292
|
+
visibility: flags.visibility,
|
|
293
|
+
update: flags.update,
|
|
294
|
+
...(flags.assetType ? { assetType: flags.assetType } : {}),
|
|
295
|
+
...(flags.installsAs ? { installsAs: flags.installsAs } : {}),
|
|
296
|
+
...(flags.version ? { version: flags.version } : {}),
|
|
297
|
+
});
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
case "share":
|
|
301
|
+
notAvailable("`floom share`");
|
|
302
|
+
case "list": {
|
|
303
|
+
const flags = parseListFlags(rest);
|
|
304
|
+
await list(flags);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
case "info":
|
|
308
|
+
{
|
|
309
|
+
const flags = parseInfoFlags(rest);
|
|
310
|
+
await info({ slug: flags.slug ?? "", json: flags.json });
|
|
311
|
+
}
|
|
312
|
+
return;
|
|
313
|
+
case "add":
|
|
314
|
+
case "install": {
|
|
315
|
+
const slug = rest.find((a) => !a.startsWith("--"));
|
|
316
|
+
if (!slug) {
|
|
317
|
+
throw new FloomError("Missing skill slug.", "Try: `floom add <url-or-slug>`");
|
|
318
|
+
}
|
|
319
|
+
await install(slug);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
case "sync":
|
|
323
|
+
await sync();
|
|
324
|
+
return;
|
|
325
|
+
case "watch": {
|
|
326
|
+
const flags = parseWatchFlags(rest);
|
|
327
|
+
await watch(flags.intervalSeconds);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
case "delete":
|
|
331
|
+
case "rm": {
|
|
332
|
+
const flags = parseDeleteFlags(rest);
|
|
333
|
+
await deleteSkill({ slug: flags.slug ?? "", yes: flags.yes });
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
case "library":
|
|
337
|
+
case "lib":
|
|
338
|
+
notAvailable("`floom library`");
|
|
339
|
+
case "move":
|
|
340
|
+
notAvailable("`floom move`");
|
|
341
|
+
case "mcp":
|
|
342
|
+
notAvailable("`floom mcp setup`");
|
|
343
|
+
case "doctor":
|
|
344
|
+
await doctor();
|
|
345
|
+
return;
|
|
346
|
+
default:
|
|
347
|
+
throw new FloomError(`Unknown command: ${cmd}`, "Run `floom --help` to see available commands.");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
catch (e) {
|
|
351
|
+
printError(e);
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
void main();
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
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 getWebUrl() {
|
|
12
|
+
return process.env.FLOOM_WEB_URL?.replace(/\/$/, "") ?? DEFAULT_WEB_URL;
|
|
13
|
+
}
|
|
14
|
+
export async function readConfig() {
|
|
15
|
+
try {
|
|
16
|
+
const buf = await readFile(process.env.FLOOM_CONFIG_PATH ?? CONFIG_PATH, "utf8");
|
|
17
|
+
const parsed = JSON.parse(buf);
|
|
18
|
+
if (!parsed.accessToken || !parsed.refreshToken)
|
|
19
|
+
return null;
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
if (e.code === "ENOENT")
|
|
24
|
+
return null;
|
|
25
|
+
throw e;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export async function writeConfig(cfg) {
|
|
29
|
+
const targetPath = process.env.FLOOM_CONFIG_PATH ?? CONFIG_PATH;
|
|
30
|
+
const targetDir = dirname(targetPath);
|
|
31
|
+
await mkdir(targetDir, { recursive: true, mode: 0o700 });
|
|
32
|
+
await writeFile(targetPath, JSON.stringify(cfg, null, 2), { mode: 0o600 });
|
|
33
|
+
// belt-and-suspenders: set perms in case the file already existed
|
|
34
|
+
await chmod(targetPath, 0o600);
|
|
35
|
+
}
|
|
36
|
+
export async function deleteConfig() {
|
|
37
|
+
try {
|
|
38
|
+
await unlink(process.env.FLOOM_CONFIG_PATH ?? CONFIG_PATH);
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
if (e.code !== "ENOENT")
|
|
42
|
+
throw e;
|
|
43
|
+
}
|
|
44
|
+
}
|
package/dist/delete.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
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 { getApiUrl, readConfig } 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: `floom delete <slug>`");
|
|
35
|
+
const cfg = await readConfig();
|
|
36
|
+
if (!cfg)
|
|
37
|
+
throw new FloomError("Not signed in.", "Run `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 = cfg.apiUrl ?? getApiUrl();
|
|
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
|
+
}
|