@floomhq/floom 1.0.63 → 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/publish.js DELETED
@@ -1,240 +0,0 @@
1
- import { parse as parseYaml } from "yaml";
2
- import ora from "ora";
3
- import clipboard from "clipboardy";
4
- import { getWebUrl, readConfig, resolveApiUrl } from "./config.js";
5
- import { floomFetch } from "./lib/api.js";
6
- import { c, symbols } from "./ui.js";
7
- import { FloomError, friendlyHttp } from "./errors.js";
8
- import { formatVersionLabel } from "./version.js";
9
- import { detectSkillSecurityFindings, formatSecurityFindings } from "./secrets.js";
10
- import { readSkillPackage } from "./package.js";
11
- const ASSET_TYPES = new Set(["knowledge", "instruction", "workflow", "skill"]);
12
- const INSTALL_TARGETS = new Set([
13
- "claude_skill",
14
- "memory",
15
- "rule",
16
- "codex_instruction",
17
- "opencode_instruction",
18
- "cursor_rule",
19
- "other",
20
- ]);
21
- const VERSION_RE = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$/;
22
- const SLUG_RE = /^[A-Za-z0-9_-]{1,128}$/;
23
- const YAML_MAX_ALIAS_COUNT = 50;
24
- function parseFrontmatter(input) {
25
- const trimmed = input.replace(/^/, "");
26
- if (!trimmed.startsWith("---"))
27
- return { meta: {}, body: input };
28
- const end = trimmed.indexOf("\n---", 3);
29
- if (end === -1) {
30
- return {
31
- meta: {},
32
- body: input,
33
- error: { message: "Frontmatter opens with `---` but never closes.", line: 1 },
34
- };
35
- }
36
- const headerBlock = trimmed.slice(3, end).trim();
37
- const rest = trimmed.slice(end + 4).replace(/^\r?\n/, "");
38
- const meta = {};
39
- let parsed;
40
- try {
41
- parsed = headerBlock ? parseYaml(headerBlock, { maxAliasCount: YAML_MAX_ALIAS_COUNT }) : {};
42
- }
43
- catch (err) {
44
- return {
45
- meta,
46
- body: rest,
47
- error: {
48
- message: err instanceof Error ? err.message.replace(/\n.*/s, "") : "Invalid YAML.",
49
- line: yamlErrorLine(err),
50
- },
51
- };
52
- }
53
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
54
- return { meta, body: rest };
55
- for (const [rawKey, rawValue] of Object.entries(parsed)) {
56
- const key = rawKey.trim().toLowerCase();
57
- const value = frontmatterScalar(rawValue);
58
- if (value === undefined)
59
- continue;
60
- if (key === "title"
61
- || key === "description"
62
- || key === "version"
63
- || key === "slug"
64
- || key === "type"
65
- || key === "asset_type"
66
- || key === "installs_as"
67
- || key === "installs-as") {
68
- if (key === "installs-as") {
69
- meta.installs_as = value;
70
- }
71
- else {
72
- meta[key] = value;
73
- }
74
- }
75
- }
76
- return { meta, body: rest };
77
- }
78
- function yamlErrorLine(err) {
79
- const linePos = err.linePos;
80
- const line = linePos?.[0]?.line;
81
- return typeof line === "number" && Number.isFinite(line) ? line + 1 : 2;
82
- }
83
- function frontmatterScalar(value) {
84
- if (typeof value === "string")
85
- return value;
86
- if (typeof value === "number" || typeof value === "boolean")
87
- return String(value);
88
- return undefined;
89
- }
90
- function parseAssetType(value, source) {
91
- if (!value)
92
- return undefined;
93
- if (ASSET_TYPES.has(value))
94
- return value;
95
- throw new FloomError(`Invalid ${source}: ${value}`, "Use one of: knowledge, instruction, workflow, skill.");
96
- }
97
- function parseInstallsAs(value, source) {
98
- if (!value)
99
- return undefined;
100
- if (INSTALL_TARGETS.has(value))
101
- return value;
102
- throw new FloomError(`Invalid ${source}: ${value}`, "Use one of: claude_skill, memory, rule, codex_instruction, opencode_instruction, cursor_rule, other.");
103
- }
104
- function parseVersion(value, source) {
105
- if (!value)
106
- return undefined;
107
- if (VERSION_RE.test(value))
108
- return value;
109
- throw new FloomError(`Invalid ${source}: ${value}`, "Use 1-64 characters: letters, numbers, dots, underscores, plus, or hyphen.");
110
- }
111
- export async function publishSkillPath(opts) {
112
- const skillPackage = await readSkillPackage(opts.file);
113
- const securityFailures = [
114
- { path: "SKILL.md", body: skillPackage.skillBody },
115
- ...skillPackage.packageFiles.map((file) => ({
116
- path: file.path,
117
- body: Buffer.from(file.content_base64, "base64").toString("utf8"),
118
- })),
119
- ].flatMap((file) => {
120
- const findings = detectSkillSecurityFindings(file.body);
121
- return findings.length > 0 ? [`${file.path}\n${formatSecurityFindings(findings)}`] : [];
122
- });
123
- if (securityFailures.length > 0) {
124
- throw new FloomError("Security scan failed. Publish stopped.", `${securityFailures.join("\n")}\nRemove secrets, prompt-injection text, or data-exfiltration instructions before publishing.`);
125
- }
126
- const fileCount = 1 + skillPackage.packageFiles.length;
127
- if (!opts.quiet) {
128
- process.stdout.write(`\n${symbols.ok} Pre-publish security scan passed (${fileCount} file${fileCount === 1 ? "" : "s"}, 3 checks).\n`);
129
- }
130
- const { meta, error: fmError } = parseFrontmatter(skillPackage.skillBody);
131
- if (fmError) {
132
- throw new FloomError(`Couldn't parse frontmatter — check your YAML.`, `Line ${fmError.line}: ${fmError.message}`);
133
- }
134
- validateTextMeta(meta.title, "frontmatter title", 200);
135
- validateTextMeta(meta.description, "frontmatter description", 1000);
136
- const assetType = opts.assetType ?? parseAssetType(meta.asset_type ?? meta.type, "frontmatter type") ?? "skill";
137
- const installsAs = opts.installsAs ?? parseInstallsAs(meta.installs_as, "frontmatter installs_as") ?? null;
138
- const version = opts.version ?? parseVersion(meta.version, "frontmatter version") ?? null;
139
- const updateSlug = opts.update ? (opts.updateSlug ?? meta.slug) : undefined;
140
- if (opts.update && !updateSlug) {
141
- throw new FloomError("Missing update slug.", "Use `--update <slug>` or add `slug:` to the skill frontmatter.");
142
- }
143
- if (updateSlug !== undefined && !SLUG_RE.test(updateSlug)) {
144
- throw new FloomError(`Invalid update slug: ${updateSlug}`, "Use the Floom skill slug from its share URL.");
145
- }
146
- const cfg = await readConfig();
147
- if (!cfg) {
148
- throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
149
- }
150
- const apiUrl = resolveApiUrl(cfg);
151
- const body = {
152
- title: meta.title ?? null,
153
- description: meta.description ?? null,
154
- body_md: skillPackage.skillBody,
155
- ...((opts.visibility ?? (!updateSlug ? "unlisted" : undefined)) ? { visibility: opts.visibility ?? "unlisted" } : {}),
156
- asset_type: assetType,
157
- source: "markdown",
158
- installs_as: installsAs,
159
- version,
160
- original_filename: skillPackage.originalFilename,
161
- published_via: "cli",
162
- ...(opts.sharedWithEmails ? { shared_with_emails: opts.sharedWithEmails } : {}),
163
- ...(skillPackage.packageFiles.length > 0 ? { package_files: skillPackage.packageFiles } : {}),
164
- };
165
- let res;
166
- try {
167
- res = await floomFetch(updateSlug ? `${apiUrl}/api/v1/skills/${updateSlug}` : `${apiUrl}/api/skills`, updateSlug ? "update your skill" : "publish your skill", {
168
- method: updateSlug ? "PATCH" : "POST",
169
- token: cfg.accessToken,
170
- checkOk: false,
171
- body,
172
- });
173
- }
174
- catch (err) {
175
- throw err;
176
- }
177
- if (res.status === 409) {
178
- throw new FloomError("Skill already published.", "Use `--update <slug>` to publish a new version of an existing skill.");
179
- }
180
- if (!res.ok) {
181
- throw friendlyHttp(res.status, updateSlug ? "update your skill" : "publish your skill");
182
- }
183
- const data = (await res.json());
184
- // Build a humans-friendly URL: strip the trailing `.md` if the API returned one.
185
- const webBase = getWebUrl();
186
- const humanUrl = data.url
187
- ? data.url.replace(/\.md$/, "")
188
- : `${webBase}/s/${data.slug}`;
189
- const titleLabel = data.title ? `"${data.title}"` : opts.file;
190
- return { data, humanUrl, version, titleLabel };
191
- }
192
- export async function publish(opts) {
193
- const spinner = ora({
194
- text: c.dim(`${opts.update ? "Updating" : "Publishing"} ${opts.file}...`),
195
- color: "yellow",
196
- }).start();
197
- let result;
198
- try {
199
- result = await publishSkillPath(opts);
200
- }
201
- catch (err) {
202
- spinner.stop();
203
- throw err;
204
- }
205
- spinner.stop();
206
- const versionTag = result.version ? c.dim(` (${formatVersionLabel(result.version)})`) : "";
207
- const verb = opts.update ? "Updated" : "Published";
208
- process.stdout.write(`\n${symbols.ok} ${verb} ${c.bold(result.titleLabel)}${versionTag}\n\n`);
209
- process.stdout.write(` ${c.cyan(result.humanUrl)}\n\n`);
210
- if (result.data.visibility === "shared" && result.data.shared_with_emails?.length) {
211
- process.stdout.write(` ${c.dim(`Shared with ${result.data.shared_with_emails.join(", ")}`)}\n\n`);
212
- }
213
- let copied = false;
214
- try {
215
- await clipboard.write(result.humanUrl);
216
- copied = true;
217
- }
218
- catch {
219
- copied = false;
220
- }
221
- if (copied) {
222
- process.stdout.write(` ${c.dim("Copied to clipboard. Share it anywhere.")}\n\n`);
223
- }
224
- else {
225
- process.stdout.write(` ${c.dim("Share it anywhere.")}\n\n`);
226
- }
227
- process.stdout.write(` ${c.bold("Next")}\n`);
228
- process.stdout.write(` ${c.dim("1.")} Test locally: ${c.cyan(`npx -y @floomhq/floom add ${result.humanUrl} --setup`)}\n`);
229
- process.stdout.write(` ${c.dim("2.")} Send the link.\n`);
230
- process.stdout.write(` ${c.dim("3.")} Receiver runs ${c.cyan(`npx -y @floomhq/floom add ${result.humanUrl} --setup`)}\n`);
231
- process.stdout.write(` ${c.dim("4.")} Agent reads the installed Markdown from the local skills folder.\n\n`);
232
- }
233
- function validateTextMeta(value, label, maxLength) {
234
- if (value === undefined)
235
- return;
236
- if (!value.trim())
237
- throw new FloomError(`Invalid ${label}.`, "Metadata values cannot be blank.");
238
- if (value.length > maxLength)
239
- throw new FloomError(`Invalid ${label}.`, `Use ${maxLength} characters or fewer.`);
240
- }
@@ -1,372 +0,0 @@
1
- import { constants } from "node:fs";
2
- import { mkdir, open, readdir, rename } from "node:fs/promises";
3
- import { createHash } from "node:crypto";
4
- import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
- import { CONFIG_DIR } from "./config.js";
6
- import { FloomError } from "./errors.js";
7
- import { publishSkillPath } from "./publish.js";
8
- import { readSkillPackage } from "./package.js";
9
- import { manifestKey, markSynced, readSyncManifest, unmarkSynced, writeSyncManifest } from "./sync-manifest.js";
10
- import { c, symbols } from "./ui.js";
11
- import { targetSkillsDir } from "./targets.js";
12
- const MANIFEST_VERSION = 1;
13
- const PUSH_MANIFEST_PATH = join(CONFIG_DIR, "push-manifest.json");
14
- const MAX_SCAN_DEPTH = 8;
15
- const SKIP_DIRS = new Set([".cache", ".git", ".next", ".pytest_cache", "__pycache__", "build", "coverage", "dist", "node_modules", "out"]);
16
- const SLUG_RE = /^[A-Za-z0-9_-]{1,128}$/;
17
- const PROJECTION_MANIFEST_FILENAMES = [".floom-cli-sync-manifest.json", ".floom-sync-manifest.json"];
18
- function emptyManifest() {
19
- return { version: MANIFEST_VERSION, files: {} };
20
- }
21
- async function readPushManifest() {
22
- try {
23
- const handle = await open(PUSH_MANIFEST_PATH, constants.O_RDONLY | constants.O_NOFOLLOW);
24
- try {
25
- const parsed = JSON.parse(await handle.readFile("utf8"));
26
- if (parsed.version !== MANIFEST_VERSION || !parsed.files || typeof parsed.files !== "object")
27
- return emptyManifest();
28
- return parsed;
29
- }
30
- finally {
31
- await handle.close();
32
- }
33
- }
34
- catch (err) {
35
- if (err.code === "ENOENT")
36
- return emptyManifest();
37
- if (err instanceof SyntaxError)
38
- return emptyManifest();
39
- throw err;
40
- }
41
- }
42
- async function writePushManifest(manifest) {
43
- await mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
44
- const tmp = join(CONFIG_DIR, `.push-manifest.${process.pid}.${Date.now()}.tmp`);
45
- const handle = await open(tmp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
46
- try {
47
- await handle.writeFile(JSON.stringify(manifest, null, 2));
48
- }
49
- finally {
50
- await handle.close();
51
- }
52
- await rename(tmp, PUSH_MANIFEST_PATH);
53
- }
54
- function hashPackage(skillPath, body, files) {
55
- const hash = createHash("sha256");
56
- hash.update(skillPath);
57
- hash.update("\0");
58
- hash.update(body);
59
- for (const file of files) {
60
- hash.update("\0");
61
- hash.update(file.path);
62
- hash.update("\0");
63
- hash.update(file.sha256);
64
- }
65
- return hash.digest("hex");
66
- }
67
- async function findSkillPackages(root) {
68
- const out = [];
69
- async function walk(dir, depth) {
70
- if (depth > MAX_SCAN_DEPTH)
71
- return;
72
- let entries;
73
- try {
74
- entries = await readdir(dir, { withFileTypes: true });
75
- }
76
- catch (err) {
77
- if (err.code === "ENOENT")
78
- return;
79
- throw err;
80
- }
81
- if (entries.some((entry) => entry.isFile() && entry.name === "SKILL.md")) {
82
- out.push(dir);
83
- }
84
- for (const entry of entries) {
85
- if (!entry.isDirectory() || entry.name.startsWith(".") || SKIP_DIRS.has(entry.name))
86
- continue;
87
- if (entry.isSymbolicLink())
88
- continue;
89
- await walk(join(dir, entry.name), depth + 1);
90
- }
91
- }
92
- await walk(root, 0);
93
- return out.sort();
94
- }
95
- function safeRootRelative(root, path) {
96
- const relativePath = relative(resolve(root), resolve(path));
97
- if (relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
98
- throw new FloomError("Invalid watched skill path.");
99
- }
100
- return relativePath.split(sep).join("/");
101
- }
102
- function slugFromSyncManifest(root, skillFilePath, syncManifest) {
103
- const key = manifestKey(root, skillFilePath);
104
- const entry = syncManifest.files[key];
105
- return entry?.slug ?? null;
106
- }
107
- async function readProjectionManifest(root, target, syncManifest) {
108
- const manifest = { version: 1, files: { ...syncManifest.files } };
109
- const candidates = [
110
- ...PROJECTION_MANIFEST_FILENAMES.map((name) => join(root, name)),
111
- join(CONFIG_DIR, "native-sync-manifests", `${target}.json`),
112
- ...PROJECTION_MANIFEST_FILENAMES.map((name) => join(CONFIG_DIR, "skill-cache", target, name)),
113
- ];
114
- const seen = new Set();
115
- for (const candidate of candidates) {
116
- const resolved = resolve(candidate);
117
- if (seen.has(resolved))
118
- continue;
119
- seen.add(resolved);
120
- const extra = await readSyncManifestFile(resolved);
121
- Object.assign(manifest.files, extra.files);
122
- }
123
- return manifest;
124
- }
125
- async function readSyncManifestFile(path) {
126
- try {
127
- const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
128
- try {
129
- const parsed = JSON.parse(await handle.readFile("utf8"));
130
- if (parsed.version !== 1 || !parsed.files || typeof parsed.files !== "object")
131
- return { version: 1, files: {} };
132
- const files = {};
133
- for (const [key, entry] of Object.entries(parsed.files)) {
134
- if (entry &&
135
- typeof entry === "object" &&
136
- typeof entry.hash === "string" &&
137
- typeof entry.slug === "string" &&
138
- typeof entry.target === "string" &&
139
- typeof entry.syncedAt === "string" &&
140
- entry.target === key &&
141
- SLUG_RE.test(entry.slug)) {
142
- files[key] = entry;
143
- }
144
- }
145
- return { version: 1, files };
146
- }
147
- finally {
148
- await handle.close();
149
- }
150
- }
151
- catch (err) {
152
- if (err.code === "ENOENT")
153
- return { version: 1, files: {} };
154
- if (err instanceof SyntaxError)
155
- return { version: 1, files: {} };
156
- throw err;
157
- }
158
- }
159
- function isFloomCacheRoot(root, target) {
160
- const relativeRoot = relative(resolve(CONFIG_DIR, "skill-cache", target), resolve(root));
161
- return relativeRoot === "" || (!relativeRoot.startsWith(`..${sep}`) && relativeRoot !== ".." && !isAbsolute(relativeRoot));
162
- }
163
- function isExplicitlyPublished(entry) {
164
- return entry?.source === "published" || entry?.source === "updated";
165
- }
166
- function isBaselineAdopted(entry) {
167
- return Boolean(entry) && !isExplicitlyPublished(entry);
168
- }
169
- function slugFromPushManifest(key, pushManifest) {
170
- return pushManifest.files[key]?.slug ?? null;
171
- }
172
- function pushManifestKey(target, relativeSkillPath) {
173
- return `${target}:${relativeSkillPath}`;
174
- }
175
- function baselineAdoptAll() {
176
- return process.env.FLOOM_BASELINE_ADOPT_ALL === "1";
177
- }
178
- function isUnchangedSyncedPackage(root, skillPackage, syncManifest) {
179
- const files = [
180
- { target: skillPackage.skillPath, hash: createHash("sha256").update(skillPackage.skillBody).digest("hex") },
181
- ...skillPackage.packageFiles.map((file) => ({
182
- target: join(dirname(skillPackage.skillPath), ...file.path.split("/")),
183
- hash: file.sha256,
184
- })),
185
- ];
186
- return files.every((file) => syncManifest.files[manifestKey(root, file.target)]?.hash === file.hash);
187
- }
188
- function markPackageSynced(root, skillPackage, syncManifest, slug) {
189
- markSynced(syncManifest, manifestKey(root, skillPackage.skillPath), slug, createHash("sha256").update(skillPackage.skillBody).digest("hex"));
190
- for (const file of skillPackage.packageFiles) {
191
- markSynced(syncManifest, manifestKey(root, join(dirname(skillPackage.skillPath), ...file.path.split("/"))), slug, file.sha256);
192
- }
193
- }
194
- function fallbackSlugFromPath(packagePath) {
195
- const slug = packagePath.split(/[\\/]/).filter(Boolean).at(-1);
196
- return slug && SLUG_RE.test(slug) ? slug : null;
197
- }
198
- export async function pushWatchOnce(opts) {
199
- const root = targetSkillsDir(opts.target);
200
- const pushManifest = await readPushManifest();
201
- const syncManifest = await readSyncManifest();
202
- const projectionManifest = await readProjectionManifest(root, opts.target, syncManifest);
203
- const cacheRoot = isFloomCacheRoot(root, opts.target);
204
- const packages = await findSkillPackages(root);
205
- const activePushKeys = new Set();
206
- const activeSyncKeys = new Set();
207
- let published = 0;
208
- let updated = 0;
209
- let adopted = 0;
210
- let skipped = 0;
211
- let syncManifestChanged = false;
212
- for (const packagePath of packages) {
213
- let skillPackage;
214
- try {
215
- skillPackage = await readSkillPackage(packagePath);
216
- }
217
- catch (err) {
218
- skipped += 1;
219
- if (!opts.quiet) {
220
- process.stderr.write(`[floom] skipped ${packagePath}: ${err instanceof Error ? err.message : String(err)}\n`);
221
- }
222
- continue;
223
- }
224
- const key = safeRootRelative(root, skillPackage.skillPath);
225
- const pushKey = pushManifestKey(opts.target, key);
226
- activeSyncKeys.add(manifestKey(root, skillPackage.skillPath));
227
- for (const file of skillPackage.packageFiles) {
228
- activeSyncKeys.add(manifestKey(root, join(dirname(skillPackage.skillPath), ...file.path.split("/"))));
229
- }
230
- const hash = hashPackage(key, skillPackage.skillBody, skillPackage.packageFiles);
231
- const pushed = pushManifest.files[pushKey];
232
- const projectionSlug = slugFromSyncManifest(root, skillPackage.skillPath, projectionManifest);
233
- if (cacheRoot || (projectionSlug !== null && !isExplicitlyPublished(pushed))) {
234
- skipped += 1;
235
- continue;
236
- }
237
- activePushKeys.add(pushKey);
238
- if (pushed?.hash === hash) {
239
- if (baselineAdoptAll() && pushed.source !== "adopted") {
240
- pushManifest.files[pushKey] = {
241
- hash,
242
- slug: pushed.slug,
243
- path: key,
244
- pushedAt: new Date().toISOString(),
245
- source: "adopted",
246
- };
247
- adopted += 1;
248
- continue;
249
- }
250
- if (!isUnchangedSyncedPackage(root, skillPackage, syncManifest)) {
251
- adopted += 1;
252
- markPackageSynced(root, skillPackage, syncManifest, pushed.slug);
253
- syncManifestChanged = true;
254
- await writeSyncManifest(syncManifest);
255
- }
256
- else {
257
- skipped += 1;
258
- }
259
- continue;
260
- }
261
- if (!opts.yolo) {
262
- const syncedSlug = slugFromSyncManifest(root, skillPackage.skillPath, syncManifest);
263
- const pushedSlug = slugFromPushManifest(pushKey, pushManifest);
264
- const fallbackSlug = fallbackSlugFromPath(packagePath);
265
- pushManifest.files[pushKey] = {
266
- hash,
267
- slug: pushedSlug ?? syncedSlug ?? fallbackSlug ?? key,
268
- path: key,
269
- pushedAt: new Date().toISOString(),
270
- source: baselineAdoptAll() ? "adopted" : pushed?.source ?? "adopted",
271
- };
272
- adopted += 1;
273
- continue;
274
- }
275
- const syncedSlug = slugFromSyncManifest(root, skillPackage.skillPath, syncManifest);
276
- const pushedSlug = slugFromPushManifest(pushKey, pushManifest);
277
- const fallbackSlug = fallbackSlugFromPath(packagePath);
278
- const slug = pushedSlug ?? syncedSlug ?? fallbackSlug;
279
- if (isBaselineAdopted(pushed) && !syncedSlug) {
280
- pushManifest.files[pushKey] = {
281
- hash,
282
- slug: pushed.slug,
283
- path: key,
284
- pushedAt: new Date().toISOString(),
285
- source: "adopted",
286
- };
287
- adopted += 1;
288
- continue;
289
- }
290
- if (!pushed && syncedSlug && isUnchangedSyncedPackage(root, skillPackage, syncManifest)) {
291
- pushManifest.files[pushKey] = {
292
- hash,
293
- slug: syncedSlug,
294
- path: key,
295
- pushedAt: new Date().toISOString(),
296
- };
297
- adopted += 1;
298
- continue;
299
- }
300
- if (pushed || syncedSlug) {
301
- if (!slug) {
302
- skipped += 1;
303
- continue;
304
- }
305
- try {
306
- await publishSkillPath({ file: packagePath, update: true, updateSlug: slug, quiet: true });
307
- updated += 1;
308
- pushManifest.files[pushKey] = { hash, slug, path: key, pushedAt: new Date().toISOString(), source: "updated" };
309
- markPackageSynced(root, skillPackage, syncManifest, slug);
310
- syncManifestChanged = true;
311
- await writeSyncManifest(syncManifest);
312
- }
313
- catch (err) {
314
- if (err instanceof Error && /Skill not found/i.test(err.message)) {
315
- const result = await publishSkillPath({ file: packagePath, visibility: "unlisted", quiet: true });
316
- published += 1;
317
- pushManifest.files[pushKey] = { hash, slug: result.data.slug, path: key, pushedAt: new Date().toISOString(), source: "published" };
318
- markPackageSynced(root, skillPackage, syncManifest, result.data.slug);
319
- syncManifestChanged = true;
320
- await writeSyncManifest(syncManifest);
321
- continue;
322
- }
323
- skipped += 1;
324
- if (!opts.quiet) {
325
- process.stderr.write(`[floom] skipped ${packagePath}: ${err instanceof Error ? err.message : String(err)}\n`);
326
- }
327
- }
328
- continue;
329
- }
330
- try {
331
- const result = await publishSkillPath({ file: packagePath, visibility: "unlisted", quiet: true });
332
- published += 1;
333
- pushManifest.files[pushKey] = { hash, slug: result.data.slug, path: key, pushedAt: new Date().toISOString(), source: "published" };
334
- markPackageSynced(root, skillPackage, syncManifest, result.data.slug);
335
- syncManifestChanged = true;
336
- await writeSyncManifest(syncManifest);
337
- }
338
- catch (err) {
339
- skipped += 1;
340
- if (!opts.quiet) {
341
- process.stderr.write(`[floom] skipped ${packagePath}: ${err instanceof Error ? err.message : String(err)}\n`);
342
- }
343
- }
344
- }
345
- for (const key of Object.keys(pushManifest.files)) {
346
- if (key.startsWith(`${opts.target}:`) && !activePushKeys.has(key))
347
- delete pushManifest.files[key];
348
- }
349
- for (const key of Object.keys(syncManifest.files)) {
350
- if (!activeSyncKeys.has(key)) {
351
- unmarkSynced(syncManifest, key);
352
- syncManifestChanged = true;
353
- }
354
- }
355
- await writePushManifest(pushManifest);
356
- if (syncManifestChanged)
357
- await writeSyncManifest(syncManifest);
358
- if (!opts.quiet && (published > 0 || updated > 0 || adopted > 0)) {
359
- process.stdout.write(`${symbols.ok} Floom push watch: ${packages.length} scanned, ${published} published, ${updated} updated, ${adopted} adopted\n`);
360
- }
361
- return { scanned: packages.length, published, updated, adopted, skipped };
362
- }
363
- export async function watchPush(intervalSeconds, opts) {
364
- for (;;) {
365
- await pushWatchOnce(opts);
366
- if (opts.once)
367
- return;
368
- await new Promise((resolve) => setTimeout(resolve, intervalSeconds * 1000));
369
- if (!opts.quiet)
370
- process.stdout.write(c.dim("[floom] push watch tick\n"));
371
- }
372
- }
package/dist/scan.js DELETED
@@ -1,24 +0,0 @@
1
- import { detectSkillSecurityFindings, formatSecurityFindings } from "./secrets.js";
2
- import { FloomError } from "./errors.js";
3
- import { c, symbols } from "./ui.js";
4
- import { readSkillPackage } from "./package.js";
5
- export async function scanSkill(inputPath) {
6
- const skillPackage = await readSkillPackage(inputPath);
7
- const failures = [
8
- { path: "SKILL.md", body: skillPackage.skillBody },
9
- ...skillPackage.packageFiles.map((file) => ({
10
- path: file.path,
11
- body: Buffer.from(file.content_base64, "base64").toString("utf8"),
12
- })),
13
- ].flatMap((file) => {
14
- const findings = detectSkillSecurityFindings(file.body);
15
- return findings.length > 0 ? [`${file.path}\n${formatSecurityFindings(findings)}`] : [];
16
- });
17
- if (failures.length > 0) {
18
- throw new FloomError("Security scan failed.", `${failures.join("\n")}\nRemove secrets, prompt-injection text, or data-exfiltration instructions before publishing.`);
19
- }
20
- const fileCount = 1 + skillPackage.packageFiles.length;
21
- process.stdout.write(`\n${symbols.ok} Security scan passed for ${c.bold(inputPath)}\n`);
22
- process.stdout.write(` ${c.dim(`Checked ${fileCount} package file${fileCount === 1 ? "" : "s"}.`)}\n`);
23
- process.stdout.write(` ${c.dim("No high-confidence secrets, prompt-injection text, or exfiltration instructions found.")}\n\n`);
24
- }
package/dist/search.js DELETED
@@ -1,54 +0,0 @@
1
- import ora from "ora";
2
- import { readConfig, resolveApiUrl } from "./config.js";
3
- import { getJson } from "./lib/api.js";
4
- import { c, symbols } from "./ui.js";
5
- function formatSkillRow(skill) {
6
- const title = skill.title ?? c.dim("(untitled)");
7
- const type = c.dim(`[${skill.asset_type}]`);
8
- const libs = skill.libraries.length
9
- ? c.dim(` ${skill.libraries.map((lib) => `@${lib.slug}`).join(", ")}`)
10
- : "";
11
- return ` ${c.cyan(skill.slug.padEnd(22))} ${title} ${type}${libs}`;
12
- }
13
- function formatLibraryRow(library) {
14
- const count = `${library.skill_count} ${library.skill_count === 1 ? "skill" : "skills"}`;
15
- return ` ${c.cyan(`@${library.slug}`.padEnd(22))} ${library.name} ${c.dim(count)}`;
16
- }
17
- export async function search(opts) {
18
- const cfg = await readConfig();
19
- const apiUrl = resolveApiUrl(cfg);
20
- const params = new URLSearchParams({ q: opts.query });
21
- if (opts.library)
22
- params.set("library", opts.library);
23
- if (opts.type)
24
- params.set("type", opts.type);
25
- const spinner = opts.json ? null : ora({ text: c.dim("Searching Floom..."), color: "yellow" }).start();
26
- let result;
27
- try {
28
- result = await getJson(`${apiUrl}/api/v1/search?${params.toString()}`, "search skills");
29
- }
30
- finally {
31
- spinner?.stop();
32
- }
33
- if (opts.json) {
34
- process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
35
- return;
36
- }
37
- process.stdout.write(`\n${symbols.dot} ${c.bold("Search results")} ${c.dim(`for "${result.query}"`)}\n\n`);
38
- if (result.skills.length === 0 && result.libraries.length === 0) {
39
- process.stdout.write(` ${c.dim("No matching public skills or libraries.")}\n\n`);
40
- return;
41
- }
42
- if (result.skills.length > 0) {
43
- process.stdout.write(` ${c.dim("Skills")}\n`);
44
- for (const skill of result.skills)
45
- process.stdout.write(`${formatSkillRow(skill)}\n`);
46
- process.stdout.write("\n");
47
- }
48
- if (result.libraries.length > 0) {
49
- process.stdout.write(` ${c.dim("Libraries")}\n`);
50
- for (const library of result.libraries)
51
- process.stdout.write(`${formatLibraryRow(library)}\n`);
52
- process.stdout.write("\n");
53
- }
54
- }