@floomhq/floom 1.0.26 → 1.0.28
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/README.md +6 -5
- package/dist/cli.js +61 -242
- package/dist/config.js +1 -51
- package/dist/doctor.js +12 -40
- package/dist/errors.js +1 -1
- package/dist/install.js +19 -74
- package/dist/login.js +8 -67
- package/dist/mcp.js +5 -2
- package/dist/package.js +177 -81
- package/dist/publish.js +41 -51
- package/dist/push-watch.js +245 -0
- package/dist/setup.js +87 -25
- package/dist/sync-manifest.js +44 -37
- package/dist/sync.js +15 -26
- package/dist/targets.js +45 -12
- package/package.json +1 -1
package/dist/package.js
CHANGED
|
@@ -6,40 +6,117 @@ import { promisify } from "node:util";
|
|
|
6
6
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
7
|
import { FloomError } from "./errors.js";
|
|
8
8
|
const ALLOWED_PACKAGE_DIRS = new Set([
|
|
9
|
+
".github",
|
|
9
10
|
"agents",
|
|
10
11
|
"assets",
|
|
12
|
+
"bin",
|
|
11
13
|
"canvas-fonts",
|
|
12
14
|
"checks",
|
|
15
|
+
"claude",
|
|
16
|
+
"codex",
|
|
17
|
+
"contrib",
|
|
13
18
|
"core",
|
|
19
|
+
"cursor",
|
|
20
|
+
"design",
|
|
21
|
+
"docs",
|
|
14
22
|
"evidence",
|
|
15
23
|
"examples",
|
|
24
|
+
"extension",
|
|
16
25
|
"helpers",
|
|
26
|
+
"hosts",
|
|
27
|
+
"kimi",
|
|
28
|
+
"lib",
|
|
29
|
+
"migrations",
|
|
30
|
+
"model-overlays",
|
|
31
|
+
"openclaw",
|
|
32
|
+
"opencode",
|
|
17
33
|
"reference",
|
|
18
34
|
"references",
|
|
19
|
-
"
|
|
35
|
+
"remotion-starter",
|
|
20
36
|
"scripts",
|
|
21
|
-
"
|
|
37
|
+
"sdk",
|
|
38
|
+
"schemas",
|
|
39
|
+
"src",
|
|
40
|
+
"specialists",
|
|
41
|
+
"supabase",
|
|
22
42
|
"templates",
|
|
43
|
+
"test",
|
|
23
44
|
"tests",
|
|
24
45
|
"themes",
|
|
46
|
+
"vendor",
|
|
25
47
|
]);
|
|
26
48
|
const ALLOWED_PACKAGE_ROOT_FILES = new Set(["SKILL.md", ".gitignore"]);
|
|
27
|
-
const
|
|
28
|
-
const ALLOWED_PACKAGE_ROOT_EXTENSIONS = new Set([
|
|
49
|
+
const ALLOWED_PACKAGE_ROOT_SUPPORT_FILES = new Set([
|
|
29
50
|
".env.example",
|
|
30
|
-
".
|
|
31
|
-
".
|
|
32
|
-
"
|
|
33
|
-
".
|
|
34
|
-
".
|
|
35
|
-
".
|
|
36
|
-
".
|
|
37
|
-
".
|
|
38
|
-
".
|
|
39
|
-
".
|
|
51
|
+
"ACKNOWLEDGEMENTS.md",
|
|
52
|
+
"CHANGELOG.md",
|
|
53
|
+
"LICENSE",
|
|
54
|
+
"LICENSE.md",
|
|
55
|
+
"LICENSE.txt",
|
|
56
|
+
"README.md",
|
|
57
|
+
"AGENTS.md",
|
|
58
|
+
"ARCHITECTURE.md",
|
|
59
|
+
"BROWSER.md",
|
|
60
|
+
"CLAUDE.md",
|
|
61
|
+
"CONTRIBUTING.md",
|
|
62
|
+
"DESIGN.md",
|
|
63
|
+
"ETHOS.md",
|
|
64
|
+
"PLAN-snapshot-dropdown-interactive.md",
|
|
65
|
+
"REMOTION_PROTOCOL.md",
|
|
66
|
+
"SKILL.md.tmpl",
|
|
67
|
+
"TODOS.md",
|
|
68
|
+
"TODOS-format.md",
|
|
69
|
+
"USING_GBRAIN_WITH_GSTACK.md",
|
|
70
|
+
"VERSION",
|
|
71
|
+
".gitlab-ci.yml",
|
|
72
|
+
"actionlint.yaml",
|
|
73
|
+
"bun.lock",
|
|
74
|
+
"checklist.md",
|
|
75
|
+
"conductor.json",
|
|
76
|
+
"design-checklist.md",
|
|
77
|
+
"dx-hall-of-fame.md",
|
|
78
|
+
"editing.md",
|
|
79
|
+
"forms.md",
|
|
80
|
+
"instructions.md",
|
|
81
|
+
"nanobanana.py",
|
|
82
|
+
"package.json",
|
|
83
|
+
"plan.md",
|
|
84
|
+
"pptxgenjs.md",
|
|
85
|
+
"reference.md",
|
|
86
|
+
"requirements.txt",
|
|
87
|
+
"greptile-triage.md",
|
|
88
|
+
"setup",
|
|
89
|
+
"skill.md",
|
|
90
|
+
"slop-scan.config.json",
|
|
91
|
+
"style_guidelines.md",
|
|
92
|
+
"theme-showcase.pdf",
|
|
93
|
+
]);
|
|
94
|
+
const GENERATED_PACKAGE_DIRS = new Set([
|
|
95
|
+
".agents",
|
|
96
|
+
".cache",
|
|
97
|
+
".cursor",
|
|
98
|
+
".factory",
|
|
99
|
+
".gbrain",
|
|
100
|
+
".git",
|
|
101
|
+
".hermes",
|
|
102
|
+
".kiro",
|
|
103
|
+
".next",
|
|
104
|
+
".openclaw",
|
|
105
|
+
".opencode",
|
|
106
|
+
".pytest_cache",
|
|
107
|
+
".slate",
|
|
108
|
+
"__pycache__",
|
|
109
|
+
"build",
|
|
110
|
+
"coverage",
|
|
111
|
+
"dist",
|
|
112
|
+
"fixtures",
|
|
113
|
+
"node_modules",
|
|
114
|
+
"out",
|
|
40
115
|
]);
|
|
41
|
-
const
|
|
42
|
-
const
|
|
116
|
+
const GENERATED_PACKAGE_FILES = new Set([".DS_Store"]);
|
|
117
|
+
const GENERATED_PACKAGE_FILE_SUFFIXES = [".icns", ".log", ".mov", ".mp3", ".mp4", ".pyc", ".pyo", ".wav", ".webm"];
|
|
118
|
+
const PACKAGE_FILE_LIMIT = 1000;
|
|
119
|
+
const PACKAGE_TOTAL_BYTES_LIMIT = 8_000_000;
|
|
43
120
|
const PACKAGE_FILE_BYTES_LIMIT = 500_000;
|
|
44
121
|
const PACKAGE_SEGMENT_RE = /^[A-Za-z0-9._-]{1,128}$/;
|
|
45
122
|
const BASE64_RE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
@@ -126,40 +203,41 @@ async function collectPackageFiles(root) {
|
|
|
126
203
|
const isIgnored = gitIgnoreChecker(root);
|
|
127
204
|
const rootEntries = await readdir(root, { withFileTypes: true });
|
|
128
205
|
for (const entry of rootEntries) {
|
|
129
|
-
if (
|
|
206
|
+
if (ALLOWED_PACKAGE_ROOT_FILES.has(entry.name) || ALLOWED_PACKAGE_DIRS.has(entry.name))
|
|
130
207
|
continue;
|
|
131
|
-
if (
|
|
208
|
+
if (ALLOWED_PACKAGE_ROOT_SUPPORT_FILES.has(entry.name)) {
|
|
209
|
+
if (entry.isSymbolicLink())
|
|
210
|
+
continue;
|
|
211
|
+
if (!entry.isFile())
|
|
212
|
+
throw new FloomError(`Package path is not a regular file: ${entry.name}`);
|
|
132
213
|
continue;
|
|
133
|
-
|
|
214
|
+
}
|
|
215
|
+
if (isGeneratedPackageEntry(entry.name, entry.isDirectory()))
|
|
134
216
|
continue;
|
|
135
|
-
if (
|
|
217
|
+
if (await isIgnored(entry.name))
|
|
136
218
|
continue;
|
|
137
|
-
if (entry.
|
|
138
|
-
const rel = entry.name;
|
|
139
|
-
validatePackageRelativePath(rel);
|
|
140
|
-
const fullPath = join(root, entry.name);
|
|
141
|
-
const stat = await lstat(fullPath);
|
|
142
|
-
if (files.length >= PACKAGE_FILE_LIMIT) {
|
|
143
|
-
throw new FloomError("Skill package has too many files.", "A skill package is capped at 100 supporting files.");
|
|
144
|
-
}
|
|
145
|
-
if (stat.size > PACKAGE_FILE_BYTES_LIMIT) {
|
|
146
|
-
throw new FloomError(`File too large: ${rel}`, "Each package file is capped at 500KB.");
|
|
147
|
-
}
|
|
148
|
-
totalBytes += stat.size;
|
|
149
|
-
ensurePackageSize(totalBytes);
|
|
150
|
-
const bytes = await readFile(fullPath);
|
|
151
|
-
files.push({
|
|
152
|
-
path: rel,
|
|
153
|
-
content_base64: bytes.toString("base64"),
|
|
154
|
-
encoding: "base64",
|
|
155
|
-
size_bytes: bytes.length,
|
|
156
|
-
sha256: sha256Bytes(bytes),
|
|
157
|
-
});
|
|
219
|
+
if (entry.isDirectory() && await isNestedSkillDir(join(root, entry.name)))
|
|
158
220
|
continue;
|
|
159
|
-
|
|
160
|
-
|
|
221
|
+
if (entry.isSymbolicLink())
|
|
222
|
+
continue;
|
|
223
|
+
const hint = "Move supporting files under references/, examples/, scripts/, or assets/.";
|
|
161
224
|
throw new FloomError(`Unsupported root package entry: ${entry.name}`, hint);
|
|
162
225
|
}
|
|
226
|
+
for (const entry of rootEntries) {
|
|
227
|
+
if (!ALLOWED_PACKAGE_ROOT_SUPPORT_FILES.has(entry.name))
|
|
228
|
+
continue;
|
|
229
|
+
if (entry.isSymbolicLink())
|
|
230
|
+
continue;
|
|
231
|
+
const fullPath = join(root, entry.name);
|
|
232
|
+
if (await isIgnored(entry.name))
|
|
233
|
+
continue;
|
|
234
|
+
await collectFile(fullPath, entry.name, files, (size) => {
|
|
235
|
+
totalBytes += size;
|
|
236
|
+
if (totalBytes > PACKAGE_TOTAL_BYTES_LIMIT) {
|
|
237
|
+
throw new FloomError("Skill package is too large.", "A skill package is capped at 8MB total.");
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
163
241
|
for (const dirName of ALLOWED_PACKAGE_DIRS) {
|
|
164
242
|
const dirPath = join(root, dirName);
|
|
165
243
|
let stat;
|
|
@@ -174,30 +252,23 @@ async function collectPackageFiles(root) {
|
|
|
174
252
|
if (await isIgnored(dirName))
|
|
175
253
|
continue;
|
|
176
254
|
if (stat.isSymbolicLink())
|
|
177
|
-
|
|
255
|
+
continue;
|
|
178
256
|
if (!stat.isDirectory())
|
|
179
257
|
throw new FloomError(`Package entry must be a directory: ${dirName}`);
|
|
180
258
|
await collectDir(root, dirPath, files, isIgnored, (size) => {
|
|
181
259
|
totalBytes += size;
|
|
182
|
-
|
|
260
|
+
if (totalBytes > PACKAGE_TOTAL_BYTES_LIMIT) {
|
|
261
|
+
throw new FloomError("Skill package is too large.", "A skill package is capped at 8MB total.");
|
|
262
|
+
}
|
|
183
263
|
});
|
|
184
264
|
}
|
|
185
265
|
return files.sort((a, b) => a.path.localeCompare(b.path));
|
|
186
266
|
}
|
|
187
|
-
function isAllowedRootPackageFile(name) {
|
|
188
|
-
if (name.startsWith(".") && name !== ".env.example")
|
|
189
|
-
return false;
|
|
190
|
-
const lower = name.toLowerCase();
|
|
191
|
-
return Array.from(ALLOWED_PACKAGE_ROOT_EXTENSIONS).some((ext) => lower.endsWith(ext));
|
|
192
|
-
}
|
|
193
|
-
function ensurePackageSize(totalBytes) {
|
|
194
|
-
if (totalBytes > PACKAGE_TOTAL_BYTES_LIMIT) {
|
|
195
|
-
throw new FloomError("Skill package is too large.", "A skill package is capped at 1MB total.");
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
267
|
async function collectDir(root, dir, files, isIgnored, addBytes) {
|
|
199
268
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
200
269
|
for (const entry of entries) {
|
|
270
|
+
if (isGeneratedPackageEntry(entry.name, entry.isDirectory()))
|
|
271
|
+
continue;
|
|
201
272
|
if (!PACKAGE_SEGMENT_RE.test(entry.name)) {
|
|
202
273
|
throw new FloomError(`Invalid package path segment: ${entry.name}`);
|
|
203
274
|
}
|
|
@@ -206,35 +277,58 @@ async function collectDir(root, dir, files, isIgnored, addBytes) {
|
|
|
206
277
|
if (await isIgnored(rel))
|
|
207
278
|
continue;
|
|
208
279
|
if (entry.isSymbolicLink())
|
|
209
|
-
|
|
280
|
+
continue;
|
|
210
281
|
if (entry.isDirectory()) {
|
|
211
282
|
await collectDir(root, fullPath, files, isIgnored, addBytes);
|
|
212
283
|
continue;
|
|
213
284
|
}
|
|
214
285
|
if (!entry.isFile())
|
|
215
286
|
throw new FloomError(`Package path is not a regular file: ${rel}`);
|
|
216
|
-
|
|
217
|
-
throw new FloomError("Skill package has too many files.", "A skill package is capped at 100 supporting files.");
|
|
218
|
-
}
|
|
219
|
-
const stat = await lstat(fullPath);
|
|
220
|
-
if (stat.isSymbolicLink())
|
|
221
|
-
throw new FloomError(`Package path is a symbolic link: ${rel}`);
|
|
222
|
-
if (!stat.isFile())
|
|
223
|
-
throw new FloomError(`Package path is not a regular file: ${rel}`);
|
|
224
|
-
if (stat.size > PACKAGE_FILE_BYTES_LIMIT) {
|
|
225
|
-
throw new FloomError(`File too large: ${rel}`, "Each package file is capped at 500KB.");
|
|
226
|
-
}
|
|
227
|
-
addBytes(stat.size);
|
|
228
|
-
const bytes = await readFile(fullPath);
|
|
229
|
-
files.push({
|
|
230
|
-
path: rel,
|
|
231
|
-
content_base64: bytes.toString("base64"),
|
|
232
|
-
encoding: "base64",
|
|
233
|
-
size_bytes: bytes.length,
|
|
234
|
-
sha256: sha256Bytes(bytes),
|
|
235
|
-
});
|
|
287
|
+
await collectFile(fullPath, rel, files, addBytes);
|
|
236
288
|
}
|
|
237
289
|
}
|
|
290
|
+
async function isNestedSkillDir(path) {
|
|
291
|
+
try {
|
|
292
|
+
const stat = await lstat(join(path, "SKILL.md"));
|
|
293
|
+
return stat.isFile();
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
if (err.code === "ENOENT")
|
|
297
|
+
return false;
|
|
298
|
+
throw err;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async function collectFile(fullPath, rel, files, addBytes) {
|
|
302
|
+
const normalized = rel.split(sep).join("/");
|
|
303
|
+
validatePackageRelativePath(normalized);
|
|
304
|
+
if (files.length >= PACKAGE_FILE_LIMIT) {
|
|
305
|
+
throw new FloomError("Skill package has too many files.", "A skill package is capped at 100 supporting files.");
|
|
306
|
+
}
|
|
307
|
+
const stat = await lstat(fullPath);
|
|
308
|
+
if (stat.isSymbolicLink())
|
|
309
|
+
throw new FloomError(`Package path is a symbolic link: ${normalized}`);
|
|
310
|
+
if (!stat.isFile())
|
|
311
|
+
throw new FloomError(`Package path is not a regular file: ${normalized}`);
|
|
312
|
+
if (stat.size > PACKAGE_FILE_BYTES_LIMIT) {
|
|
313
|
+
throw new FloomError(`File too large: ${normalized}`, "Each package file is capped at 500KB.");
|
|
314
|
+
}
|
|
315
|
+
addBytes(stat.size);
|
|
316
|
+
const bytes = await readFile(fullPath);
|
|
317
|
+
files.push({
|
|
318
|
+
path: normalized,
|
|
319
|
+
content_base64: bytes.toString("base64"),
|
|
320
|
+
encoding: "base64",
|
|
321
|
+
size_bytes: bytes.length,
|
|
322
|
+
sha256: sha256Bytes(bytes),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
function isGeneratedPackageEntry(name, isDirectory) {
|
|
326
|
+
if (isDirectory && GENERATED_PACKAGE_DIRS.has(name))
|
|
327
|
+
return true;
|
|
328
|
+
if (GENERATED_PACKAGE_FILES.has(name))
|
|
329
|
+
return true;
|
|
330
|
+
return GENERATED_PACKAGE_FILE_SUFFIXES.some((suffix) => name.endsWith(suffix));
|
|
331
|
+
}
|
|
238
332
|
function gitIgnoreChecker(root) {
|
|
239
333
|
const cache = new Map();
|
|
240
334
|
let localRules = null;
|
|
@@ -304,11 +398,13 @@ export function validatePackageRelativePath(path) {
|
|
|
304
398
|
throw new FloomError(`Invalid package file path: ${path}`);
|
|
305
399
|
}
|
|
306
400
|
const segments = path.split("/");
|
|
307
|
-
if (segments.length === 1
|
|
308
|
-
|
|
401
|
+
if (segments.length === 1) {
|
|
402
|
+
if (ALLOWED_PACKAGE_ROOT_SUPPORT_FILES.has(path) && PACKAGE_SEGMENT_RE.test(path))
|
|
403
|
+
return;
|
|
404
|
+
throw new FloomError(`Invalid package file path: ${path}`, "Package root files must be approved support files.");
|
|
309
405
|
}
|
|
310
|
-
if (
|
|
311
|
-
throw new FloomError(`Invalid package file path: ${path}`,
|
|
406
|
+
if (!ALLOWED_PACKAGE_DIRS.has(segments[0] ?? "")) {
|
|
407
|
+
throw new FloomError(`Invalid package file path: ${path}`, "Package files must be under an approved support directory.");
|
|
312
408
|
}
|
|
313
409
|
if (segments.some((segment) => segment === "." || segment === ".." || !PACKAGE_SEGMENT_RE.test(segment))) {
|
|
314
410
|
throw new FloomError(`Invalid package file path: ${path}`);
|
package/dist/publish.js
CHANGED
|
@@ -107,34 +107,7 @@ function parseVersion(value, source) {
|
|
|
107
107
|
return value;
|
|
108
108
|
throw new FloomError(`Invalid ${source}: ${value}`, "Use 1-64 characters: letters, numbers, dots, underscores, plus, or hyphen.");
|
|
109
109
|
}
|
|
110
|
-
function
|
|
111
|
-
const trimmed = input.trim();
|
|
112
|
-
try {
|
|
113
|
-
const url = new URL(trimmed);
|
|
114
|
-
const last = url.pathname.split("/").filter(Boolean).at(-1) ?? "";
|
|
115
|
-
return last.replace(/\.(md|json)$/i, "");
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
return trimmed.replace(/\.(md|json)$/i, "");
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
function inferSlugFromPath(path) {
|
|
122
|
-
return path
|
|
123
|
-
.replace(/[\\/]+$/, "")
|
|
124
|
-
.split(/[\\/]+/)
|
|
125
|
-
.filter(Boolean)
|
|
126
|
-
.at(-1)
|
|
127
|
-
?.replace(/\.md$/i, "") ?? "";
|
|
128
|
-
}
|
|
129
|
-
function resolveUpdateSlug(opts, meta) {
|
|
130
|
-
const raw = opts.updateSlug ?? meta.slug ?? inferSlugFromPath(opts.file);
|
|
131
|
-
const slug = slugFromInput(raw);
|
|
132
|
-
if (!SLUG_RE.test(slug)) {
|
|
133
|
-
throw new FloomError("Missing or invalid update slug.", "Use `npx -y @floomhq/floom publish <path> --update <slug-or-url>`, or add `slug:` to SKILL.md frontmatter.");
|
|
134
|
-
}
|
|
135
|
-
return slug;
|
|
136
|
-
}
|
|
137
|
-
export async function publish(opts) {
|
|
110
|
+
export async function publishSkillPath(opts) {
|
|
138
111
|
const skillPackage = await readSkillPackage(opts.file);
|
|
139
112
|
const securityFailures = [
|
|
140
113
|
{ path: "SKILL.md", body: skillPackage.skillBody },
|
|
@@ -150,7 +123,9 @@ export async function publish(opts) {
|
|
|
150
123
|
throw new FloomError("Security scan failed. Publish stopped.", `${securityFailures.join("\n")}\nRemove secrets, prompt-injection text, or data-exfiltration instructions before publishing.`);
|
|
151
124
|
}
|
|
152
125
|
const fileCount = 1 + skillPackage.packageFiles.length;
|
|
153
|
-
|
|
126
|
+
if (!opts.quiet) {
|
|
127
|
+
process.stdout.write(`\n${symbols.ok} Pre-publish security scan passed (${fileCount} file${fileCount === 1 ? "" : "s"}, 3 checks).\n`);
|
|
128
|
+
}
|
|
154
129
|
const { meta, error: fmError } = parseFrontmatter(skillPackage.skillBody);
|
|
155
130
|
if (fmError) {
|
|
156
131
|
throw new FloomError(`Couldn't parse frontmatter — check your YAML.`, `Line ${fmError.line}: ${fmError.message}`);
|
|
@@ -160,20 +135,23 @@ export async function publish(opts) {
|
|
|
160
135
|
const assetType = opts.assetType ?? parseAssetType(meta.asset_type ?? meta.type, "frontmatter type") ?? "skill";
|
|
161
136
|
const installsAs = opts.installsAs ?? parseInstallsAs(meta.installs_as, "frontmatter installs_as") ?? null;
|
|
162
137
|
const version = opts.version ?? parseVersion(meta.version, "frontmatter version") ?? null;
|
|
138
|
+
const updateSlug = opts.update ? (opts.updateSlug ?? meta.slug) : undefined;
|
|
139
|
+
if (opts.update && !updateSlug) {
|
|
140
|
+
throw new FloomError("Missing update slug.", "Use `--update <slug>` or add `slug:` to the skill frontmatter.");
|
|
141
|
+
}
|
|
142
|
+
if (updateSlug !== undefined && !SLUG_RE.test(updateSlug)) {
|
|
143
|
+
throw new FloomError(`Invalid update slug: ${updateSlug}`, "Use the Floom skill slug from its share URL.");
|
|
144
|
+
}
|
|
163
145
|
const cfg = await readConfig();
|
|
164
146
|
if (!cfg) {
|
|
165
147
|
throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
|
|
166
148
|
}
|
|
167
|
-
const spinner = ora({
|
|
168
|
-
text: c.dim(`${opts.update ? "Updating" : "Publishing"} ${meta.title ? c.bold(meta.title) : opts.file}...`),
|
|
169
|
-
color: "yellow",
|
|
170
|
-
}).start();
|
|
171
149
|
const apiUrl = resolveApiUrl(cfg);
|
|
172
150
|
const body = {
|
|
173
151
|
title: meta.title ?? null,
|
|
174
152
|
description: meta.description ?? null,
|
|
175
153
|
body_md: skillPackage.skillBody,
|
|
176
|
-
visibility: opts.visibility,
|
|
154
|
+
...((opts.visibility ?? (!updateSlug ? "unlisted" : undefined)) ? { visibility: opts.visibility ?? "unlisted" } : {}),
|
|
177
155
|
asset_type: assetType,
|
|
178
156
|
source: "markdown",
|
|
179
157
|
installs_as: installsAs,
|
|
@@ -181,12 +159,11 @@ export async function publish(opts) {
|
|
|
181
159
|
original_filename: skillPackage.originalFilename,
|
|
182
160
|
published_via: "cli",
|
|
183
161
|
...(opts.sharedWithEmails ? { shared_with_emails: opts.sharedWithEmails } : {}),
|
|
184
|
-
...(
|
|
162
|
+
...(skillPackage.packageFiles.length > 0 ? { package_files: skillPackage.packageFiles } : {}),
|
|
185
163
|
};
|
|
186
164
|
let res;
|
|
187
165
|
try {
|
|
188
|
-
|
|
189
|
-
res = await floomFetch(updateSlug ? `${apiUrl}/api/v1/skills/${encodeURIComponent(updateSlug)}` : `${apiUrl}/api/skills`, updateSlug ? "update your skill" : "publish your skill", {
|
|
166
|
+
res = await floomFetch(updateSlug ? `${apiUrl}/api/v1/skills/${updateSlug}` : `${apiUrl}/api/skills`, updateSlug ? "update your skill" : "publish your skill", {
|
|
190
167
|
method: updateSlug ? "PATCH" : "POST",
|
|
191
168
|
token: cfg.accessToken,
|
|
192
169
|
checkOk: false,
|
|
@@ -194,16 +171,13 @@ export async function publish(opts) {
|
|
|
194
171
|
});
|
|
195
172
|
}
|
|
196
173
|
catch (err) {
|
|
197
|
-
spinner.stop();
|
|
198
174
|
throw err;
|
|
199
175
|
}
|
|
200
176
|
if (res.status === 409) {
|
|
201
|
-
|
|
202
|
-
throw new FloomError("Skill already published.", "Use `--update <slug-or-url>` to publish a new version of an existing skill.");
|
|
177
|
+
throw new FloomError("Skill already published.", "Use `--update <slug>` to publish a new version of an existing skill.");
|
|
203
178
|
}
|
|
204
179
|
if (!res.ok) {
|
|
205
|
-
|
|
206
|
-
throw friendlyHttp(res.status, opts.update ? "update your skill" : "publish your skill");
|
|
180
|
+
throw friendlyHttp(res.status, updateSlug ? "update your skill" : "publish your skill");
|
|
207
181
|
}
|
|
208
182
|
const data = (await res.json());
|
|
209
183
|
// Build a humans-friendly URL: strip the trailing `.md` if the API returned one.
|
|
@@ -211,17 +185,33 @@ export async function publish(opts) {
|
|
|
211
185
|
const humanUrl = data.url
|
|
212
186
|
? data.url.replace(/\.md$/, "")
|
|
213
187
|
: `${webBase}/s/${data.slug}`;
|
|
214
|
-
spinner.stop();
|
|
215
|
-
const versionTag = version ? c.dim(` (${formatVersionLabel(version)})`) : "";
|
|
216
188
|
const titleLabel = data.title ? `"${data.title}"` : opts.file;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
189
|
+
return { data, humanUrl, version, titleLabel };
|
|
190
|
+
}
|
|
191
|
+
export async function publish(opts) {
|
|
192
|
+
const spinner = ora({
|
|
193
|
+
text: c.dim(`${opts.update ? "Updating" : "Publishing"} ${opts.file}...`),
|
|
194
|
+
color: "yellow",
|
|
195
|
+
}).start();
|
|
196
|
+
let result;
|
|
197
|
+
try {
|
|
198
|
+
result = await publishSkillPath(opts);
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
spinner.stop();
|
|
202
|
+
throw err;
|
|
203
|
+
}
|
|
204
|
+
spinner.stop();
|
|
205
|
+
const versionTag = result.version ? c.dim(` (${formatVersionLabel(result.version)})`) : "";
|
|
206
|
+
const verb = opts.update ? "Updated" : "Published";
|
|
207
|
+
process.stdout.write(`\n${symbols.ok} ${verb} ${c.bold(result.titleLabel)}${versionTag}\n\n`);
|
|
208
|
+
process.stdout.write(` ${c.cyan(result.humanUrl)}\n\n`);
|
|
209
|
+
if (result.data.visibility === "shared" && result.data.shared_with_emails?.length) {
|
|
210
|
+
process.stdout.write(` ${c.dim(`Shared with ${result.data.shared_with_emails.join(", ")}`)}\n\n`);
|
|
221
211
|
}
|
|
222
212
|
let copied = false;
|
|
223
213
|
try {
|
|
224
|
-
await clipboard.write(humanUrl);
|
|
214
|
+
await clipboard.write(result.humanUrl);
|
|
225
215
|
copied = true;
|
|
226
216
|
}
|
|
227
217
|
catch {
|
|
@@ -234,9 +224,9 @@ export async function publish(opts) {
|
|
|
234
224
|
process.stdout.write(` ${c.dim("Share it anywhere.")}\n\n`);
|
|
235
225
|
}
|
|
236
226
|
process.stdout.write(` ${c.bold("Next")}\n`);
|
|
237
|
-
process.stdout.write(` ${c.dim("1.")} Test locally: ${c.cyan(`npx -y @floomhq/floom add ${humanUrl} --setup`)}\n`);
|
|
227
|
+
process.stdout.write(` ${c.dim("1.")} Test locally: ${c.cyan(`npx -y @floomhq/floom add ${result.humanUrl} --setup`)}\n`);
|
|
238
228
|
process.stdout.write(` ${c.dim("2.")} Send the link.\n`);
|
|
239
|
-
process.stdout.write(` ${c.dim("3.")} Receiver runs ${c.cyan(`npx -y @floomhq/floom add ${humanUrl} --setup`)}\n`);
|
|
229
|
+
process.stdout.write(` ${c.dim("3.")} Receiver runs ${c.cyan(`npx -y @floomhq/floom add ${result.humanUrl} --setup`)}\n`);
|
|
240
230
|
process.stdout.write(` ${c.dim("4.")} Agent reads the installed Markdown from the local skills folder.\n\n`);
|
|
241
231
|
}
|
|
242
232
|
function validateTextMeta(value, label, maxLength) {
|