@floomhq/skills 0.2.0 → 0.2.2

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.
Files changed (82) hide show
  1. package/dist/index.d.ts +0 -1
  2. package/dist/index.js +3649 -87
  3. package/dist/index.js.map +7 -1
  4. package/dist/version.d.ts +1 -1
  5. package/dist/version.js +1 -2
  6. package/package.json +17 -6
  7. package/dist/api-client.d.ts +0 -9
  8. package/dist/api-client.js +0 -61
  9. package/dist/api-client.js.map +0 -1
  10. package/dist/commands/info.d.ts +0 -1
  11. package/dist/commands/info.js +0 -24
  12. package/dist/commands/info.js.map +0 -1
  13. package/dist/commands/init.d.ts +0 -1
  14. package/dist/commands/init.js +0 -108
  15. package/dist/commands/init.js.map +0 -1
  16. package/dist/commands/install.d.ts +0 -8
  17. package/dist/commands/install.js +0 -136
  18. package/dist/commands/install.js.map +0 -1
  19. package/dist/commands/installed.d.ts +0 -4
  20. package/dist/commands/installed.js +0 -22
  21. package/dist/commands/installed.js.map +0 -1
  22. package/dist/commands/library.d.ts +0 -4
  23. package/dist/commands/library.js +0 -29
  24. package/dist/commands/library.js.map +0 -1
  25. package/dist/commands/list.d.ts +0 -7
  26. package/dist/commands/list.js +0 -32
  27. package/dist/commands/list.js.map +0 -1
  28. package/dist/commands/login.d.ts +0 -1
  29. package/dist/commands/login.js +0 -57
  30. package/dist/commands/login.js.map +0 -1
  31. package/dist/commands/logout.d.ts +0 -1
  32. package/dist/commands/logout.js +0 -19
  33. package/dist/commands/logout.js.map +0 -1
  34. package/dist/commands/outdated.d.ts +0 -1
  35. package/dist/commands/outdated.js +0 -50
  36. package/dist/commands/outdated.js.map +0 -1
  37. package/dist/commands/publish.d.ts +0 -4
  38. package/dist/commands/publish.js +0 -84
  39. package/dist/commands/publish.js.map +0 -1
  40. package/dist/commands/share.d.ts +0 -5
  41. package/dist/commands/share.js +0 -34
  42. package/dist/commands/share.js.map +0 -1
  43. package/dist/commands/update.d.ts +0 -4
  44. package/dist/commands/update.js +0 -92
  45. package/dist/commands/update.js.map +0 -1
  46. package/dist/commands/validate.d.ts +0 -32
  47. package/dist/commands/validate.js +0 -108
  48. package/dist/commands/validate.js.map +0 -1
  49. package/dist/commands/whoami.d.ts +0 -1
  50. package/dist/commands/whoami.js +0 -14
  51. package/dist/commands/whoami.js.map +0 -1
  52. package/dist/config.d.ts +0 -11
  53. package/dist/config.js +0 -39
  54. package/dist/config.js.map +0 -1
  55. package/dist/lib/floom-lock.d.ts +0 -22
  56. package/dist/lib/floom-lock.js +0 -66
  57. package/dist/lib/floom-lock.js.map +0 -1
  58. package/dist/lib/output.d.ts +0 -11
  59. package/dist/lib/output.js +0 -17
  60. package/dist/lib/output.js.map +0 -1
  61. package/dist/version.js.map +0 -1
  62. package/src/api-client.ts +0 -80
  63. package/src/commands/info.ts +0 -36
  64. package/src/commands/init.ts +0 -109
  65. package/src/commands/install.ts +0 -176
  66. package/src/commands/installed.ts +0 -29
  67. package/src/commands/library.ts +0 -41
  68. package/src/commands/list.ts +0 -59
  69. package/src/commands/login.ts +0 -77
  70. package/src/commands/logout.ts +0 -18
  71. package/src/commands/outdated.ts +0 -57
  72. package/src/commands/publish.ts +0 -111
  73. package/src/commands/share.ts +0 -41
  74. package/src/commands/update.ts +0 -116
  75. package/src/commands/validate.ts +0 -132
  76. package/src/commands/whoami.ts +0 -14
  77. package/src/config.ts +0 -44
  78. package/src/index.ts +0 -109
  79. package/src/lib/floom-lock.ts +0 -81
  80. package/src/lib/output.ts +0 -17
  81. package/src/version.ts +0 -1
  82. package/tsconfig.json +0 -9
@@ -1,66 +0,0 @@
1
- import { readFile, writeFile, stat, readdir } from "node:fs/promises";
2
- import { join, relative, sep, posix } from "node:path";
3
- import { createHash } from "node:crypto";
4
- const EMPTY = { schema_version: "0.1", skills: {} };
5
- export async function readLock(projectDir) {
6
- try {
7
- const raw = await readFile(join(projectDir, "floom.lock"), "utf8");
8
- const parsed = JSON.parse(raw);
9
- return parsed.schema_version === "0.1" ? parsed : { ...EMPTY };
10
- }
11
- catch (e) {
12
- if (e.code === "ENOENT")
13
- return { ...EMPTY };
14
- throw e;
15
- }
16
- }
17
- export async function writeLock(projectDir, lock) {
18
- await writeFile(join(projectDir, "floom.lock"), JSON.stringify(lock, null, 2) + "\n", "utf8");
19
- }
20
- export function setLockEntry(lock, ref, entry) {
21
- return { ...lock, skills: { ...lock.skills, [ref]: entry } };
22
- }
23
- export function removeLockEntry(lock, ref) {
24
- const { [ref]: _, ...rest } = lock.skills;
25
- return { ...lock, skills: rest };
26
- }
27
- export async function hashInstalledFolder(folderAbs) {
28
- const out = [];
29
- async function walk(dir) {
30
- let entries;
31
- try {
32
- entries = await readdir(dir, { withFileTypes: true });
33
- }
34
- catch {
35
- return;
36
- }
37
- for (const entry of entries) {
38
- const abs = join(dir, entry.name);
39
- if (entry.isSymbolicLink())
40
- continue;
41
- if (entry.isDirectory()) {
42
- await walk(abs);
43
- continue;
44
- }
45
- if (!entry.isFile())
46
- continue;
47
- const buf = await readFile(abs);
48
- const rel = relative(folderAbs, abs).split(sep).join(posix.sep);
49
- out.push({
50
- relPath: rel,
51
- sha256: createHash("sha256").update(buf).digest("hex"),
52
- size: buf.length,
53
- });
54
- }
55
- }
56
- try {
57
- await stat(folderAbs);
58
- }
59
- catch {
60
- return out;
61
- }
62
- await walk(folderAbs);
63
- out.sort((a, b) => a.relPath.localeCompare(b.relPath));
64
- return out;
65
- }
66
- //# sourceMappingURL=floom-lock.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"floom-lock.js","sourceRoot":"","sources":["../../src/lib/floom-lock.ts"],"names":[],"mappings":"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"}
@@ -1,11 +0,0 @@
1
- export declare const log: {
2
- info: (msg: string) => void;
3
- ok: (msg: string) => void;
4
- warn: (msg: string) => void;
5
- err: (msg: string) => void;
6
- step: (msg: string) => void;
7
- heading: (msg: string) => void;
8
- kv: (key: string, value: string) => void;
9
- blank: () => void;
10
- };
11
- export declare function exitWith(code: number, msg?: string): never;
@@ -1,17 +0,0 @@
1
- import chalk from "chalk";
2
- export const log = {
3
- info: (msg) => console.log(msg),
4
- ok: (msg) => console.log(chalk.green("✓ ") + msg),
5
- warn: (msg) => console.log(chalk.yellow("! ") + msg),
6
- err: (msg) => console.error(chalk.red("✗ ") + msg),
7
- step: (msg) => console.log(chalk.dim("· ") + msg),
8
- heading: (msg) => console.log("\n" + chalk.bold(msg)),
9
- kv: (key, value) => console.log(` ${chalk.dim(key.padEnd(16))}${value}`),
10
- blank: () => console.log(""),
11
- };
12
- export function exitWith(code, msg) {
13
- if (msg)
14
- log.err(msg);
15
- process.exit(code);
16
- }
17
- //# sourceMappingURL=output.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"output.js","sourceRoot":"","sources":["../../src/lib/output.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,MAAM,CAAC,MAAM,GAAG,GAAG;IACjB,IAAI,EAAE,CAAC,GAAW,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC;IACvC,EAAE,EAAE,CAAC,GAAW,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC;IACzD,IAAI,EAAE,CAAC,GAAW,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC;IAC5D,GAAG,EAAE,CAAC,GAAW,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC;IAC1D,IAAI,EAAE,CAAC,GAAW,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC;IACzD,OAAO,EAAE,CAAC,GAAW,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC7D,EAAE,EAAE,CAAC,GAAW,EAAE,KAAa,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,GAAG,KAAK,EAAE,CAAC;IACzF,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;CAC7B,CAAC;AAEF,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,GAAY;IACjD,IAAI,GAAG;QAAE,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACtB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACrB,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC"}
package/src/api-client.ts DELETED
@@ -1,80 +0,0 @@
1
- import { FloomError, type ErrorCode } from "@floom/shared";
2
- import { getApiUrl, readAuth } from "./config.js";
3
- import { VERSION } from "./version.js";
4
-
5
- export interface ApiOptions {
6
- method?: "GET" | "POST" | "PATCH" | "DELETE" | "PUT";
7
- body?: unknown;
8
- authRequired?: boolean;
9
- query?: Record<string, string | number | boolean | undefined>;
10
- }
11
-
12
- export async function api<T = unknown>(path: string, opts: ApiOptions = {}): Promise<T> {
13
- const auth = await readAuth();
14
- if (opts.authRequired && !auth) {
15
- throw new FloomError("AUTH_REQUIRED" as ErrorCode, "Not logged in. Run: floom login");
16
- }
17
-
18
- const url = new URL((auth?.apiUrl ?? getApiUrl()) + path);
19
- if (opts.query) {
20
- for (const [k, v] of Object.entries(opts.query)) {
21
- if (v !== undefined) url.searchParams.set(k, String(v));
22
- }
23
- }
24
-
25
- const headers: Record<string, string> = {
26
- "Content-Type": "application/json",
27
- "User-Agent": `floom-cli/${VERSION}`,
28
- "x-floom-cli-version": VERSION,
29
- };
30
- if (auth) headers.Authorization = `Bearer ${auth.token}`;
31
-
32
- const res = await fetch(url.toString(), {
33
- method: opts.method ?? "GET",
34
- headers,
35
- body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
36
- });
37
-
38
- const text = await res.text();
39
- let json: any = null;
40
- try { json = text ? JSON.parse(text) : null; } catch { /* not JSON */ }
41
-
42
- if (!res.ok) {
43
- const err = json?.error ?? {};
44
- throw new FloomError(
45
- (err.code ?? "INTERNAL_ERROR") as ErrorCode,
46
- err.message ?? `HTTP ${res.status} ${res.statusText}`,
47
- { status: res.status, requestId: err.request_id },
48
- );
49
- }
50
- return json as T;
51
- }
52
-
53
- // Variants for binary upload/download to Supabase Storage signed URLs (no auth header)
54
- export async function rawPut(url: string, body: Buffer | Uint8Array, contentType = "application/octet-stream"): Promise<void> {
55
- // Convert to BufferSource that fetch accepts
56
- const u8 = body instanceof Buffer ? new Uint8Array(body) : body;
57
- const res = await fetch(url, {
58
- method: "PUT",
59
- headers: { "Content-Type": contentType },
60
- body: u8 as BodyInit,
61
- });
62
- if (!res.ok) {
63
- throw new FloomError(
64
- "UPLOAD_FAILED" as ErrorCode,
65
- `Upload failed: HTTP ${res.status} ${res.statusText}`,
66
- );
67
- }
68
- }
69
-
70
- export async function rawGet(url: string): Promise<Buffer> {
71
- const res = await fetch(url);
72
- if (!res.ok) {
73
- throw new FloomError(
74
- "DOWNLOAD_FAILED" as ErrorCode,
75
- `Download failed: HTTP ${res.status} ${res.statusText}`,
76
- );
77
- }
78
- const ab = await res.arrayBuffer();
79
- return Buffer.from(ab);
80
- }
@@ -1,36 +0,0 @@
1
- import { parseSkillRef } from "@floom/shared";
2
- import { api } from "../api-client.js";
3
- import { log } from "../lib/output.js";
4
-
5
- interface InfoResponse {
6
- ref: string;
7
- title: string;
8
- description: string;
9
- visibility: "private" | "unlisted" | "public";
10
- latest_version: string;
11
- owner: { handle: string; display_name?: string };
12
- has_scripts: boolean;
13
- declared_scripts?: Array<{ path: string; description: string }>;
14
- }
15
-
16
- export async function infoCommand(refStr: string): Promise<void> {
17
- const ref = parseSkillRef(refStr);
18
- if (!ref) {
19
- log.err(`Invalid skill ref: ${refStr}`);
20
- process.exit(1);
21
- }
22
- const r = await api<InfoResponse>(`/skills/${ref.owner}/${ref.slug}`);
23
-
24
- log.heading(r.ref);
25
- log.kv("Title", r.title);
26
- log.kv("Description", r.description);
27
- log.kv("Latest", r.latest_version);
28
- log.kv("Visibility", r.visibility);
29
- log.kv("Owner", `@${r.owner.handle}${r.owner.display_name ? ` (${r.owner.display_name})` : ""}`);
30
- if (r.has_scripts) {
31
- log.kv("Scripts", "yes (review before activating)");
32
- }
33
- log.blank();
34
- log.info("Install:");
35
- log.info(` floom install ${r.ref}`);
36
- }
@@ -1,109 +0,0 @@
1
- import { writeFile, stat, readFile } from "node:fs/promises";
2
- import { join, basename } from "node:path";
3
- import prompts from "prompts";
4
- import { isValidSlug, slugErrorMessage, type SkillManifest } from "@floom/shared";
5
- import { log } from "../lib/output.js";
6
-
7
- async function pathExists(p: string): Promise<boolean> {
8
- try { await stat(p); return true; } catch { return false; }
9
- }
10
-
11
- export async function initCommand(): Promise<void> {
12
- const cwd = process.cwd();
13
- const folderName = basename(cwd);
14
-
15
- if (await pathExists(join(cwd, "skill.json"))) {
16
- log.err("skill.json already exists in this folder. Edit it directly or run from a fresh directory.");
17
- process.exit(1);
18
- }
19
-
20
- const defaultName = folderName.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
21
-
22
- const answers = await prompts([
23
- {
24
- type: "text",
25
- name: "name",
26
- message: "Skill name (lowercase, hyphens):",
27
- initial: isValidSlug(defaultName) ? defaultName : "",
28
- validate: (v: string) => slugErrorMessage(v) ?? true,
29
- },
30
- {
31
- type: "text",
32
- name: "title",
33
- message: "Title (human-readable):",
34
- initial: (prev: string) => prev.split("-").map((w: string) => w[0]?.toUpperCase() + w.slice(1)).join(" "),
35
- validate: (v: string) => v.trim().length > 0 ? true : "title required",
36
- },
37
- {
38
- type: "text",
39
- name: "description",
40
- message: "One-line description:",
41
- validate: (v: string) => v.trim().length > 0 ? true : "description required",
42
- },
43
- {
44
- type: "text",
45
- name: "version",
46
- message: "Version:",
47
- initial: "0.1.0",
48
- },
49
- ]);
50
-
51
- if (!answers.name) { log.err("Init cancelled."); process.exit(1); }
52
-
53
- const manifest: SkillManifest = {
54
- schema_version: "0.1",
55
- name: answers.name,
56
- title: answers.title,
57
- description: answers.description,
58
- version: answers.version,
59
- entrypoint: "SKILL.md",
60
- targets: ["generic"],
61
- };
62
-
63
- const skillMd = `---
64
- name: ${answers.name}
65
- description: ${answers.description}
66
- ---
67
-
68
- # ${answers.title}
69
-
70
- ## Purpose
71
-
72
- ${answers.description}
73
-
74
- ## When to use
75
-
76
- (Describe when this skill should be activated.)
77
-
78
- ## Process
79
-
80
- 1. (Step one)
81
- 2. (Step two)
82
-
83
- ## Output
84
-
85
- (Describe what the skill produces.)
86
- `;
87
-
88
- const floomIgnore = `# Floom CLI ignore patterns
89
- # One pattern per line, glob-style
90
- tmp/
91
- drafts/
92
- *.log
93
- `;
94
-
95
- await writeFile(join(cwd, "skill.json"), JSON.stringify(manifest, null, 2) + "\n", "utf8");
96
- await writeFile(join(cwd, "SKILL.md"), skillMd, "utf8");
97
- await writeFile(join(cwd, ".floomignore"), floomIgnore, "utf8");
98
-
99
- log.blank();
100
- log.ok("Skill scaffolded.");
101
- log.kv("skill.json", "manifest (edit metadata)");
102
- log.kv("SKILL.md", "instructions for the agent");
103
- log.kv(".floomignore", "patterns to exclude from publish");
104
- log.blank();
105
- log.info("Next:");
106
- log.info(" 1. Edit SKILL.md");
107
- log.info(" 2. floom validate");
108
- log.info(" 3. floom publish");
109
- }
@@ -1,176 +0,0 @@
1
- import { mkdir, readdir, rm, rename } from "node:fs/promises";
2
- import { join, resolve } from "node:path";
3
- import { tmpdir } from "node:os";
4
- import {
5
- parseSkillRef,
6
- formatSkillRef,
7
- resolveInstallDir,
8
- describeTargetForCli,
9
- extractBundle,
10
- verifyBundleHash,
11
- FloomError,
12
- type InstallTarget,
13
- } from "@floom/shared";
14
- import { api, rawGet } from "../api-client.js";
15
- import { readAuth } from "../config.js";
16
- import { log } from "../lib/output.js";
17
- import { readLock, writeLock, setLockEntry, hashInstalledFolder } from "../lib/floom-lock.js";
18
- import { VERSION } from "../version.js";
19
-
20
- export interface InstallOptions {
21
- for?: InstallTarget;
22
- to?: string;
23
- global?: boolean;
24
- force?: boolean;
25
- }
26
-
27
- interface SkillResponse {
28
- ref: string;
29
- visibility: "private" | "unlisted" | "public";
30
- latest_version: string;
31
- has_scripts: boolean;
32
- min_floom_version?: string;
33
- }
34
-
35
- interface DownloadResponse {
36
- ref: string;
37
- version: string;
38
- download: { url: string; expires_at: string };
39
- bundle_sha256: string;
40
- has_scripts: boolean;
41
- }
42
-
43
- const CLI_VERSION = VERSION;
44
-
45
- function semverGte(a: string, b: string): boolean {
46
- const pa = a.split(".").map(Number);
47
- const pb = b.split(".").map(Number);
48
- for (let i = 0; i < 3; i++) {
49
- if ((pa[i] ?? 0) > (pb[i] ?? 0)) return true;
50
- if ((pa[i] ?? 0) < (pb[i] ?? 0)) return false;
51
- }
52
- return true;
53
- }
54
-
55
- export async function installCommand(refStr: string, opts: InstallOptions = {}): Promise<void> {
56
- const ref = parseSkillRef(refStr);
57
- if (!ref) {
58
- log.err(`Invalid skill ref: ${refStr}`);
59
- log.info("Expected: @owner/slug, library-slug/slug, or with @version suffix");
60
- process.exit(1);
61
- }
62
-
63
- // Get metadata (works anonymously for public skills)
64
- let info: SkillResponse;
65
- try {
66
- info = await api<SkillResponse>(`/skills/${ref.owner}/${ref.slug}`);
67
- } catch (e) {
68
- if (e instanceof FloomError && e.code === "SKILL_ACCESS_DENIED") {
69
- const auth = await readAuth();
70
- if (!auth) {
71
- log.err(`Cannot access ${formatSkillRef(ref)}.`);
72
- log.info("Run `floom login` to install private or unlisted skills.");
73
- process.exit(1);
74
- }
75
- log.err(`You do not have access to ${formatSkillRef(ref)}.`);
76
- log.info(`Ask the owner to share it with: ${auth.email}`);
77
- process.exit(1);
78
- }
79
- throw e;
80
- }
81
-
82
- if (info.min_floom_version && !semverGte(CLI_VERSION, info.min_floom_version)) {
83
- log.err(`This skill requires Floom CLI >= ${info.min_floom_version} (you have ${CLI_VERSION}).`);
84
- log.info("Upgrade:");
85
- log.info(" npm install -g @floomhq/skills@latest");
86
- process.exit(1);
87
- }
88
-
89
- // Resolve install destination
90
- const target = resolveInstallDir({
91
- target: opts.for ?? "generic",
92
- to: opts.to,
93
- global: opts.global,
94
- });
95
- const projectDir = opts.global ? process.cwd() : process.cwd();
96
- const destFolder = join(target.dir, ref.slug);
97
-
98
- // Conflict check
99
- const lock = await readLock(projectDir);
100
- const refKey = formatSkillRef({ owner: ref.owner, slug: ref.slug });
101
- const existing = lock.skills[refKey];
102
- let folderExists = false;
103
- try {
104
- const entries = await readdir(destFolder);
105
- folderExists = entries.length > 0;
106
- } catch { /* not exists */ }
107
-
108
- if (folderExists) {
109
- if (existing && existing.path === destFolder.replace(projectDir + "/", "")) {
110
- // Tracked. Check local edits.
111
- const current = await hashInstalledFolder(destFolder);
112
- // For V0 we don't have file-level hashes in lock; we conservatively warn unless --force.
113
- if (!opts.force) {
114
- log.warn(`Folder already exists at ${destFolder} and may contain local edits.`);
115
- log.info(`Re-run with --force to overwrite, or remove the folder manually.`);
116
- process.exit(1);
117
- }
118
- } else if (!opts.force) {
119
- log.err(`Folder already exists at ${destFolder} (not tracked by floom.lock).`);
120
- log.info("Re-run with --force to overwrite, or remove the folder manually.");
121
- process.exit(1);
122
- }
123
- }
124
-
125
- // Download
126
- log.step("Fetching skill metadata...");
127
- const dl = await api<DownloadResponse>(`/skills/${ref.owner}/${ref.slug}/download`, {
128
- query: { version: ref.version },
129
- });
130
-
131
- log.step("Downloading bundle...");
132
- const buf = await rawGet(dl.download.url);
133
- if (!verifyBundleHash(buf, dl.bundle_sha256)) {
134
- log.err("Bundle hash mismatch — refusing to extract.");
135
- process.exit(1);
136
- }
137
- log.ok(`Bundle verified (${(buf.length / 1024).toFixed(1)} KB).`);
138
-
139
- // Extract to temp, then swap
140
- const tmp = await import("node:fs/promises").then((m) =>
141
- m.mkdtemp(join(tmpdir(), `floom-install-${ref.slug}-`)),
142
- );
143
- try {
144
- await extractBundle(buf, tmp);
145
- await mkdir(target.dir, { recursive: true });
146
- if (folderExists) {
147
- await rm(destFolder, { recursive: true, force: true });
148
- }
149
- await rename(tmp, destFolder);
150
- } catch (e) {
151
- await rm(tmp, { recursive: true, force: true }).catch(() => {});
152
- throw e;
153
- }
154
-
155
- // Update floom.lock
156
- const next = setLockEntry(lock, refKey, {
157
- version: dl.version,
158
- resolved: `${process.env.FLOOM_APP_URL ?? "https://floom-v0.vercel.app"}/${refKey}`,
159
- bundle_sha256: dl.bundle_sha256,
160
- installed_at: new Date().toISOString(),
161
- path: destFolder.replace(projectDir + "/", ""),
162
- preset: opts.for,
163
- });
164
- await writeLock(projectDir, next);
165
-
166
- log.blank();
167
- log.ok(`Installed ${refKey}@${dl.version}`);
168
- log.kv("Path", destFolder);
169
- log.kv("Target", target.target);
170
- log.kv("Compatible with", target.compatibleAgents.join(", "));
171
- if (dl.has_scripts) {
172
- log.blank();
173
- log.warn("This skill contains executable scripts. Floom does not run them; your agent may.");
174
- log.warn("Review the scripts/ folder before activating.");
175
- }
176
- }
@@ -1,29 +0,0 @@
1
- import { readLock } from "../lib/floom-lock.js";
2
- import { log } from "../lib/output.js";
3
-
4
- export interface InstalledOptions {
5
- json?: boolean;
6
- }
7
-
8
- export async function installedCommand(opts: InstalledOptions = {}): Promise<void> {
9
- const lock = await readLock(process.cwd());
10
- const entries = Object.entries(lock.skills);
11
-
12
- if (opts.json) {
13
- console.log(JSON.stringify(lock, null, 2));
14
- return;
15
- }
16
-
17
- if (entries.length === 0) {
18
- log.info("No skills installed in this project.");
19
- log.info("Get one: floom install @owner/slug");
20
- return;
21
- }
22
-
23
- log.heading("Installed skills in this project");
24
- for (const [ref, e] of entries) {
25
- console.log(` ${ref.padEnd(40)} ${e.version.padEnd(8)} ${(e.preset ?? "-").padEnd(8)} ${e.path}`);
26
- }
27
- log.blank();
28
- log.info(`${entries.length} skill${entries.length === 1 ? "" : "s"} installed.`);
29
- }
@@ -1,41 +0,0 @@
1
- import { api } from "../api-client.js";
2
- import { log } from "../lib/output.js";
3
-
4
- interface LibraryRow {
5
- id: string;
6
- slug: string;
7
- name: string;
8
- type: "personal" | "shared";
9
- role: "viewer" | "editor" | "admin";
10
- archived_at: string | null;
11
- }
12
-
13
- export async function libraryListCommand(): Promise<void> {
14
- const resp = await api<{ libraries: LibraryRow[] }>("/libraries", { authRequired: true });
15
- if (!resp.libraries.length) {
16
- log.info("No libraries.");
17
- return;
18
- }
19
- for (const l of resp.libraries) {
20
- console.log(`${l.slug.padEnd(24)} ${l.type.padEnd(8)} ${l.role.padEnd(6)} ${l.name}`);
21
- }
22
- }
23
-
24
- export async function libraryCreateCommand(slug: string, name: string): Promise<void> {
25
- const resp = await api<LibraryRow>("/libraries", { method: "POST", authRequired: true, body: { slug, name } });
26
- log.ok(`Created library ${resp.slug}`);
27
- }
28
-
29
- export async function libraryInviteCommand(librarySlug: string, email: string, role = "viewer"): Promise<void> {
30
- await api(`/libraries/${librarySlug}/pending-invites`, {
31
- method: "POST",
32
- authRequired: true,
33
- body: { email, role },
34
- });
35
- log.ok(`Invited ${email} to ${librarySlug} as ${role}`);
36
- }
37
-
38
- export async function libraryLeaveCommand(librarySlug: string): Promise<void> {
39
- await api(`/libraries/${librarySlug}/members/me`, { method: "DELETE", authRequired: true });
40
- log.ok(`Left library ${librarySlug}`);
41
- }
@@ -1,59 +0,0 @@
1
- import { api } from "../api-client.js";
2
- import { log } from "../lib/output.js";
3
-
4
- export interface ListOptions {
5
- query?: string;
6
- library?: string;
7
- folder?: string;
8
- flat?: boolean;
9
- }
10
-
11
- interface SkillRow {
12
- id: string;
13
- slug: string;
14
- title: string;
15
- description: string | null;
16
- visibility: "private" | "unlisted" | "public";
17
- latest_version_id: string | null;
18
- updated_at: string;
19
- library: { id: string; slug: string; name: string };
20
- }
21
-
22
- interface ListResponse {
23
- skills: SkillRow[];
24
- total: number;
25
- }
26
-
27
- export async function listCommand(opts: ListOptions = {}): Promise<void> {
28
- const resp = await api<ListResponse>("/skills", {
29
- authRequired: true,
30
- query: { q: opts.query, library: opts.library, folder: opts.folder },
31
- });
32
-
33
- if (resp.skills.length === 0) {
34
- log.info("No skills.");
35
- return;
36
- }
37
-
38
- if (opts.flat) {
39
- for (const s of resp.skills) {
40
- console.log(`${s.library.slug}/${s.slug}`);
41
- }
42
- return;
43
- }
44
-
45
- const groups = new Map<string, SkillRow[]>();
46
- for (const skill of resp.skills) {
47
- const k = `${skill.library.slug} (${skill.library.name})`;
48
- const arr = groups.get(k) ?? [];
49
- arr.push(skill);
50
- groups.set(k, arr);
51
- }
52
-
53
- for (const [library, rows] of groups.entries()) {
54
- log.heading(library);
55
- for (const s of rows) {
56
- console.log(` ${`${s.library.slug}/${s.slug}`.padEnd(40)} ${s.visibility.padEnd(10)} ${s.title}`);
57
- }
58
- }
59
- }