@floomhq/floom 1.0.18 → 1.0.20
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/cli.js +189 -14
- package/dist/doctor.js +16 -3
- package/dist/install.js +63 -15
- package/dist/login.js +13 -2
- package/dist/sync.js +12 -1
- package/dist/targets.js +16 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -71,7 +71,9 @@ function commandUsage() {
|
|
|
71
71
|
${c.dim("Skills")}
|
|
72
72
|
${c.cyan("add")} ${c.dim("<url>")} Install a skill into the local agent skills folder
|
|
73
73
|
${c.dim("Alias: install")}
|
|
74
|
-
${c.dim("Flags: --target claude|codex (default: claude), --setup, --force")}
|
|
74
|
+
${c.dim("Flags: --target claude|codex (default: claude), --setup, --force, --json")}
|
|
75
|
+
${c.cyan("update")} ${c.dim("<url>")} Refresh or migrate a local skill
|
|
76
|
+
${c.dim("Flags: --target claude|codex (default: claude), --json")}
|
|
75
77
|
${c.cyan("search")} ${c.dim("<query>")} Find public skills and libraries
|
|
76
78
|
${c.cyan("info")} ${c.dim("<url>")} Show skill metadata
|
|
77
79
|
|
|
@@ -98,7 +100,7 @@ function commandUsage() {
|
|
|
98
100
|
${c.dim("Alias: connect")}
|
|
99
101
|
${c.dim("Flags: --target claude|codex, --yes, --dry-run")}
|
|
100
102
|
${c.cyan("doctor")} Troubleshoot auth, API, and local folders
|
|
101
|
-
${c.dim("Flags: --target claude|codex (default: claude)")}
|
|
103
|
+
${c.dim("Flags: --target claude|codex (default: claude), --json")}
|
|
102
104
|
|
|
103
105
|
${c.dim("Advanced")}
|
|
104
106
|
${c.cyan("library")} Create, browse, and subscribe to libraries
|
|
@@ -108,6 +110,10 @@ function commandUsage() {
|
|
|
108
110
|
${c.cyan("sync")} Preview pull of published, saved, and library skills
|
|
109
111
|
${c.dim("Flags: --target claude|codex (default: claude)")}
|
|
110
112
|
${c.cyan("watch")} Preview polling sync loop
|
|
113
|
+
${c.dim("Flags: --target claude|codex (default: claude), --interval <seconds>")}
|
|
114
|
+
${c.cyan("agent-prompt")} Print the one-line agent instruction
|
|
115
|
+
${c.dim("Alias: paste")}
|
|
116
|
+
${c.dim("Flags: --target claude|codex (default: claude)")}
|
|
111
117
|
|
|
112
118
|
${c.bold("Examples")}
|
|
113
119
|
${c.cyan("npx -y @floomhq/floom add")} ${c.dim("https://floom.dev/s/ffas93ud --setup")}
|
|
@@ -123,6 +129,112 @@ function commandUsage() {
|
|
|
123
129
|
`;
|
|
124
130
|
process.stdout.write(out);
|
|
125
131
|
}
|
|
132
|
+
function subcommandUsage(cmd) {
|
|
133
|
+
const key = cmd === "install" ? "add" : cmd === "rm" ? "delete" : cmd === "connect" ? "setup" : cmd === "paste" ? "agent-prompt" : cmd;
|
|
134
|
+
const usageByCommand = {
|
|
135
|
+
add: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} add`)} ${c.dim("<url-or-slug> [flags]")}
|
|
136
|
+
|
|
137
|
+
Install a Floom skill into a local agent skills folder.
|
|
138
|
+
|
|
139
|
+
${c.bold("Flags")}
|
|
140
|
+
${c.cyan("--target")} ${c.dim("claude|codex")} Install for Claude Code or Codex. Default: claude.
|
|
141
|
+
${c.cyan("--setup")} Add Floom guidance to the matching agent instructions file.
|
|
142
|
+
${c.cyan("--force")} Replace the local copy when remote content differs.
|
|
143
|
+
${c.cyan("--json")} Print machine-readable install output.
|
|
144
|
+
|
|
145
|
+
${c.bold("Examples")}
|
|
146
|
+
${c.cyan(`${CLI_COMMAND} add https://floom.dev/s/ffas93ud --setup`)}
|
|
147
|
+
${c.cyan(`${CLI_COMMAND} add ffas93ud --target codex --json`)}
|
|
148
|
+
`,
|
|
149
|
+
update: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} update`)} ${c.dim("<url-or-slug> [flags]")}
|
|
150
|
+
|
|
151
|
+
Refresh or migrate one local skill through the same installer as add.
|
|
152
|
+
|
|
153
|
+
${c.bold("Flags")}
|
|
154
|
+
${c.cyan("--target")} ${c.dim("claude|codex")} Update Claude Code or Codex local skills. Default: claude.
|
|
155
|
+
${c.cyan("--setup")} Also refresh agent setup instructions.
|
|
156
|
+
${c.cyan("--json")} Print machine-readable install output.
|
|
157
|
+
`,
|
|
158
|
+
doctor: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} doctor`)} ${c.dim("[flags]")}
|
|
159
|
+
|
|
160
|
+
Diagnose auth, API reachability, PATH collisions, MCP setup, and local skills folders.
|
|
161
|
+
|
|
162
|
+
${c.bold("Flags")}
|
|
163
|
+
${c.cyan("--target")} ${c.dim("claude|codex")} Check Claude Code or Codex paths. Default: claude.
|
|
164
|
+
${c.cyan("--json")} Print structured checks for scripts.
|
|
165
|
+
`,
|
|
166
|
+
publish: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} publish`)} ${c.dim("<path> [flags]")}
|
|
167
|
+
|
|
168
|
+
Scan and publish a Markdown skill file or skill folder. Prints a Floom share URL.
|
|
169
|
+
|
|
170
|
+
${c.bold("Flags")}
|
|
171
|
+
${c.cyan("--public")} | ${c.cyan("--private")} | ${c.cyan("--unlisted")} Set visibility. Default: unlisted.
|
|
172
|
+
${c.cyan("--type")} ${c.dim("knowledge|instruction|workflow|skill")}
|
|
173
|
+
${c.cyan("--installs-as")} ${c.dim("claude_skill|memory|rule|codex_instruction|opencode_instruction|cursor_rule|other")}
|
|
174
|
+
${c.cyan("--skill-version")} ${c.dim("<label>")}
|
|
175
|
+
`,
|
|
176
|
+
init: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} init`)} ${c.dim("[path] [flags]")}
|
|
177
|
+
|
|
178
|
+
Create a starter skill folder with SKILL.md.
|
|
179
|
+
|
|
180
|
+
${c.bold("Flags")}
|
|
181
|
+
${c.cyan("--template")} ${c.dim(`generic|${INIT_TEMPLATES.filter((t) => t !== "generic").join("|")}`)}
|
|
182
|
+
`,
|
|
183
|
+
sync: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} sync`)} ${c.dim("[flags]")}
|
|
184
|
+
|
|
185
|
+
Preview pull of published, saved, and subscribed library skills.
|
|
186
|
+
|
|
187
|
+
${c.bold("Flags")}
|
|
188
|
+
${c.cyan("--target")} ${c.dim("claude|codex")} Sync into Claude Code or Codex skills. Default: claude.
|
|
189
|
+
`,
|
|
190
|
+
watch: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} watch`)} ${c.dim("[flags]")}
|
|
191
|
+
|
|
192
|
+
Poll sync on an interval. Preview behavior.
|
|
193
|
+
|
|
194
|
+
${c.bold("Flags")}
|
|
195
|
+
${c.cyan("--target")} ${c.dim("claude|codex")} Watch Claude Code or Codex skills. Default: claude.
|
|
196
|
+
${c.cyan("--interval")} ${c.dim("<seconds>")} Poll interval. Minimum: 10.
|
|
197
|
+
`,
|
|
198
|
+
setup: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} setup`)} ${c.dim("[flags]")}
|
|
199
|
+
|
|
200
|
+
Add Floom guidance to CLAUDE.md or AGENTS.md.
|
|
201
|
+
|
|
202
|
+
${c.bold("Flags")}
|
|
203
|
+
${c.cyan("--target")} ${c.dim("claude|codex")} Configure Claude Code or Codex.
|
|
204
|
+
${c.cyan("--file")} ${c.dim("<path>")} Write a specific instructions file.
|
|
205
|
+
${c.cyan("--yes")} Write without prompting.
|
|
206
|
+
${c.cyan("--dry-run")} Preview the change only.
|
|
207
|
+
`,
|
|
208
|
+
search: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} search`)} ${c.dim("<query> [flags]")}
|
|
209
|
+
|
|
210
|
+
Find public skills and libraries.
|
|
211
|
+
|
|
212
|
+
${c.bold("Flags")}
|
|
213
|
+
${c.cyan("--library")} ${c.dim("<slug>")} Limit search to one library.
|
|
214
|
+
${c.cyan("--type")} ${c.dim("knowledge|instruction|workflow|skill")}
|
|
215
|
+
${c.cyan("--json")} Print machine-readable results.
|
|
216
|
+
`,
|
|
217
|
+
info: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} info`)} ${c.dim("<url-or-slug> [flags]")}
|
|
218
|
+
|
|
219
|
+
Show public skill metadata.
|
|
220
|
+
|
|
221
|
+
${c.bold("Flags")}
|
|
222
|
+
${c.cyan("--json")} Print machine-readable metadata.
|
|
223
|
+
`,
|
|
224
|
+
"agent-prompt": `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} agent-prompt`)} ${c.dim("[flags]")}
|
|
225
|
+
|
|
226
|
+
Print the one-line instruction to paste into your agent.
|
|
227
|
+
|
|
228
|
+
${c.bold("Flags")}
|
|
229
|
+
${c.cyan("--target")} ${c.dim("claude|codex")} Print the right local skills path. Default: claude.
|
|
230
|
+
`,
|
|
231
|
+
};
|
|
232
|
+
const body = key ? usageByCommand[key] : undefined;
|
|
233
|
+
if (!body)
|
|
234
|
+
return false;
|
|
235
|
+
process.stdout.write(`${body}\n`);
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
126
238
|
const ASSET_TYPES = new Set(["knowledge", "instruction", "workflow", "skill"]);
|
|
127
239
|
const INSTALL_TARGETS = new Set([
|
|
128
240
|
"claude_skill",
|
|
@@ -312,6 +424,7 @@ function parseAddArgs(argv) {
|
|
|
312
424
|
let target;
|
|
313
425
|
let setup = false;
|
|
314
426
|
let force = false;
|
|
427
|
+
let json = false;
|
|
315
428
|
for (let i = 0; i < argv.length; i++) {
|
|
316
429
|
const a = argv[i] ?? "";
|
|
317
430
|
if (a === "--target" || a.startsWith("--target=")) {
|
|
@@ -328,6 +441,9 @@ function parseAddArgs(argv) {
|
|
|
328
441
|
else if (a === "--force") {
|
|
329
442
|
force = true;
|
|
330
443
|
}
|
|
444
|
+
else if (a === "--json") {
|
|
445
|
+
json = true;
|
|
446
|
+
}
|
|
331
447
|
else if (a.startsWith("--")) {
|
|
332
448
|
throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} add <url-or-slug> --setup\`.`);
|
|
333
449
|
}
|
|
@@ -340,7 +456,10 @@ function parseAddArgs(argv) {
|
|
|
340
456
|
if (!slug) {
|
|
341
457
|
throw new FloomError("Missing skill slug.", `Try: \`${CLI_COMMAND} add <url-or-slug> --setup\``);
|
|
342
458
|
}
|
|
343
|
-
|
|
459
|
+
if (json && setup) {
|
|
460
|
+
throw new FloomError("Cannot combine --json with --setup.", "Run the install first, then run `npx -y @floomhq/floom setup --target claude --yes`.");
|
|
461
|
+
}
|
|
462
|
+
return target ? { slug, target, setup, force, json } : { slug, setup, force, json };
|
|
344
463
|
}
|
|
345
464
|
function parseSearchFlags(argv) {
|
|
346
465
|
const out = { json: false };
|
|
@@ -426,7 +545,7 @@ function parseSetupFlags(argv) {
|
|
|
426
545
|
return out;
|
|
427
546
|
}
|
|
428
547
|
function parseDoctorFlags(argv) {
|
|
429
|
-
const out = {};
|
|
548
|
+
const out = { json: false };
|
|
430
549
|
for (let i = 0; i < argv.length; i++) {
|
|
431
550
|
const a = argv[i] ?? "";
|
|
432
551
|
if (a === "--target" || a.startsWith("--target=")) {
|
|
@@ -437,8 +556,11 @@ function parseDoctorFlags(argv) {
|
|
|
437
556
|
out.target = value;
|
|
438
557
|
i = nextIndex;
|
|
439
558
|
}
|
|
559
|
+
else if (a === "--json") {
|
|
560
|
+
out.json = true;
|
|
561
|
+
}
|
|
440
562
|
else if (a.startsWith("--")) {
|
|
441
|
-
throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} doctor --target codex\`.`);
|
|
563
|
+
throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} doctor --target codex --json\`.`);
|
|
442
564
|
}
|
|
443
565
|
else {
|
|
444
566
|
throw new FloomError(`Unexpected argument: ${a}`, `Try \`${CLI_COMMAND} doctor --target claude\`.`);
|
|
@@ -598,11 +720,19 @@ function parseWatchFlags(argv) {
|
|
|
598
720
|
out.intervalSeconds = interval;
|
|
599
721
|
i = nextIndex;
|
|
600
722
|
}
|
|
723
|
+
else if (a === "--target" || a.startsWith("--target=")) {
|
|
724
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--target");
|
|
725
|
+
if (value !== "claude" && value !== "codex") {
|
|
726
|
+
throw new FloomError("Invalid --target.", "Use claude or codex.");
|
|
727
|
+
}
|
|
728
|
+
out.target = value;
|
|
729
|
+
i = nextIndex;
|
|
730
|
+
}
|
|
601
731
|
else if (a.startsWith("--")) {
|
|
602
|
-
throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} watch --interval 60\`.`);
|
|
732
|
+
throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} watch --target claude --interval 60\`.`);
|
|
603
733
|
}
|
|
604
734
|
else {
|
|
605
|
-
throw new FloomError(`Unexpected argument: ${a}`, `Try \`${CLI_COMMAND} watch --interval 60\`.`);
|
|
735
|
+
throw new FloomError(`Unexpected argument: ${a}`, `Try \`${CLI_COMMAND} watch --target claude --interval 60\`.`);
|
|
606
736
|
}
|
|
607
737
|
}
|
|
608
738
|
return out;
|
|
@@ -610,6 +740,31 @@ function parseWatchFlags(argv) {
|
|
|
610
740
|
function notAvailable(feature) {
|
|
611
741
|
throw new FloomError(V1_NOT_AVAILABLE, `${feature} is planned for a later Floom release.`);
|
|
612
742
|
}
|
|
743
|
+
function parseAgentPromptFlags(argv) {
|
|
744
|
+
const out = {};
|
|
745
|
+
for (let i = 0; i < argv.length; i++) {
|
|
746
|
+
const a = argv[i] ?? "";
|
|
747
|
+
if (a === "--target" || a.startsWith("--target=")) {
|
|
748
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--target");
|
|
749
|
+
if (value !== "claude" && value !== "codex") {
|
|
750
|
+
throw new FloomError("Invalid --target.", "Use `claude` or `codex`.");
|
|
751
|
+
}
|
|
752
|
+
out.target = value;
|
|
753
|
+
i = nextIndex;
|
|
754
|
+
}
|
|
755
|
+
else if (a.startsWith("--")) {
|
|
756
|
+
throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} agent-prompt --target codex\`.`);
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
throw new FloomError(`Unexpected argument: ${a}`, `Try \`${CLI_COMMAND} agent-prompt --target claude\`.`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return out;
|
|
763
|
+
}
|
|
764
|
+
function printAgentPrompt(target = "claude") {
|
|
765
|
+
const folder = target === "codex" ? "~/.codex/skills" : "~/.claude/skills";
|
|
766
|
+
process.stdout.write(`Use my installed Floom skills when they fit the task. Search ${folder} first.\n`);
|
|
767
|
+
}
|
|
613
768
|
function parseSingleFileArg(argv, usageHint) {
|
|
614
769
|
let file;
|
|
615
770
|
for (const a of argv) {
|
|
@@ -634,7 +789,7 @@ function sleep(ms, signal) {
|
|
|
634
789
|
}, { once: true });
|
|
635
790
|
});
|
|
636
791
|
}
|
|
637
|
-
async function watch(intervalSeconds) {
|
|
792
|
+
async function watch(intervalSeconds, target) {
|
|
638
793
|
const cfg = await readConfig();
|
|
639
794
|
if (!cfg) {
|
|
640
795
|
throw new FloomError("Not signed in.", `Run \`${CLI_COMMAND} login\` before \`${CLI_COMMAND} watch\`, or use \`${CLI_COMMAND} add <link>\` without an account.`);
|
|
@@ -651,9 +806,9 @@ async function watch(intervalSeconds) {
|
|
|
651
806
|
};
|
|
652
807
|
process.on("SIGINT", stop);
|
|
653
808
|
process.on("SIGTERM", stop);
|
|
654
|
-
process.stdout.write(`${symbols.bullet} Watching Floom sync every ${intervalSeconds}s. Press Ctrl-C to stop.\n`);
|
|
809
|
+
process.stdout.write(`${symbols.bullet} Watching Floom sync for ${target ?? "claude"} every ${intervalSeconds}s. Press Ctrl-C to stop.\n`);
|
|
655
810
|
while (!controller.signal.aborted) {
|
|
656
|
-
await sync({ spinner: false, quietUnchanged: true });
|
|
811
|
+
await sync({ spinner: false, quietUnchanged: true, ...(target ? { target } : {}) });
|
|
657
812
|
await sleep(intervalSeconds * 1000, controller.signal);
|
|
658
813
|
}
|
|
659
814
|
}
|
|
@@ -673,10 +828,9 @@ async function main() {
|
|
|
673
828
|
// never block on update-notifier
|
|
674
829
|
}
|
|
675
830
|
}
|
|
676
|
-
// Subcommand --help: any rest arg = --help/-h/help → show top-level usage.
|
|
677
|
-
// Subcommands are simple enough that one help screen is fine for Version 1.
|
|
678
831
|
if (rest.includes("--help") || rest.includes("-h") || rest.includes("help")) {
|
|
679
|
-
|
|
832
|
+
if (!subcommandUsage(cmd))
|
|
833
|
+
commandUsage();
|
|
680
834
|
return;
|
|
681
835
|
}
|
|
682
836
|
try {
|
|
@@ -781,6 +935,20 @@ async function main() {
|
|
|
781
935
|
...(flags.target ? { target: flags.target } : {}),
|
|
782
936
|
setup: flags.setup,
|
|
783
937
|
force: flags.force,
|
|
938
|
+
json: flags.json,
|
|
939
|
+
});
|
|
940
|
+
if (flags.setup) {
|
|
941
|
+
await setupAgent({ target: flags.target ?? "claude", dryRun: false, yes: true });
|
|
942
|
+
}
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
case "update": {
|
|
946
|
+
const flags = parseAddArgs(rest);
|
|
947
|
+
await install(flags.slug, {
|
|
948
|
+
...(flags.target ? { target: flags.target } : {}),
|
|
949
|
+
setup: flags.setup,
|
|
950
|
+
force: true,
|
|
951
|
+
json: flags.json,
|
|
784
952
|
});
|
|
785
953
|
if (flags.setup) {
|
|
786
954
|
await setupAgent({ target: flags.target ?? "claude", dryRun: false, yes: true });
|
|
@@ -798,7 +966,7 @@ async function main() {
|
|
|
798
966
|
}
|
|
799
967
|
case "watch": {
|
|
800
968
|
const flags = parseWatchFlags(rest);
|
|
801
|
-
await watch(flags.intervalSeconds);
|
|
969
|
+
await watch(flags.intervalSeconds, flags.target);
|
|
802
970
|
return;
|
|
803
971
|
}
|
|
804
972
|
case "delete":
|
|
@@ -830,6 +998,13 @@ async function main() {
|
|
|
830
998
|
rejectArgs(rest, `Try \`${CLI_COMMAND} mcp\`.`);
|
|
831
999
|
printMcpSetup();
|
|
832
1000
|
return;
|
|
1001
|
+
case "agent-prompt":
|
|
1002
|
+
case "paste":
|
|
1003
|
+
{
|
|
1004
|
+
const flags = parseAgentPromptFlags(rest);
|
|
1005
|
+
printAgentPrompt(flags.target);
|
|
1006
|
+
}
|
|
1007
|
+
return;
|
|
833
1008
|
case "doctor":
|
|
834
1009
|
await doctor(parseDoctorFlags(rest));
|
|
835
1010
|
return;
|
package/dist/doctor.js
CHANGED
|
@@ -335,7 +335,6 @@ async function readExecutableVersion(path) {
|
|
|
335
335
|
}
|
|
336
336
|
export async function doctor(opts = {}) {
|
|
337
337
|
const target = opts.target ?? "claude";
|
|
338
|
-
process.stdout.write(`\n${c.bold("floom doctor")} ${c.dim(`(${formatVersionLabel(CLI_VERSION)}, target: ${target})`)}\n\n`);
|
|
339
338
|
const checks = await Promise.all([
|
|
340
339
|
checkCliCommand(),
|
|
341
340
|
checkAuth(),
|
|
@@ -344,6 +343,22 @@ export async function doctor(opts = {}) {
|
|
|
344
343
|
checkLastSync(target),
|
|
345
344
|
checkVersion(),
|
|
346
345
|
]);
|
|
346
|
+
const anyFail = checks.some((check) => check.status === "fail");
|
|
347
|
+
const anyWarn = checks.some((check) => check.status === "warn");
|
|
348
|
+
if (opts.json) {
|
|
349
|
+
process.stdout.write(`${JSON.stringify({
|
|
350
|
+
ok: !anyFail,
|
|
351
|
+
status: anyFail ? "fail" : anyWarn ? "warn" : "ok",
|
|
352
|
+
version: CLI_VERSION,
|
|
353
|
+
target,
|
|
354
|
+
checks,
|
|
355
|
+
configPath: CONFIG_PATH,
|
|
356
|
+
})}\n`);
|
|
357
|
+
if (anyFail)
|
|
358
|
+
process.exit(1);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
process.stdout.write(`\n${c.bold("floom doctor")} ${c.dim(`(${formatVersionLabel(CLI_VERSION)}, target: ${target})`)}\n\n`);
|
|
347
362
|
// Compute column widths for clean table output.
|
|
348
363
|
const nameW = Math.max(...checks.map((c) => c.name.length), 6);
|
|
349
364
|
for (const check of checks) {
|
|
@@ -353,8 +368,6 @@ export async function doctor(opts = {}) {
|
|
|
353
368
|
process.stdout.write(` ${c.dim("→ " + check.hint)}\n`);
|
|
354
369
|
}
|
|
355
370
|
}
|
|
356
|
-
const anyFail = checks.some((c) => c.status === "fail");
|
|
357
|
-
const anyWarn = checks.some((c) => c.status === "warn");
|
|
358
371
|
process.stdout.write("\n");
|
|
359
372
|
if (anyFail) {
|
|
360
373
|
process.stdout.write(` ${c.red("✗ Some checks failed.")} See hints above.\n\n`);
|
package/dist/install.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { constants } from "node:fs";
|
|
2
|
-
import { lstat, mkdir, open } from "node:fs/promises";
|
|
2
|
+
import { lstat, mkdir, open, readdir, rmdir, unlink } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import ora from "ora";
|
|
@@ -66,12 +66,14 @@ async function readLocalFile(path) {
|
|
|
66
66
|
throw err;
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
-
async function
|
|
69
|
+
async function localPackageState(root, slug, target, files) {
|
|
70
70
|
const main = await readLocalFile(target);
|
|
71
71
|
if (main === null) {
|
|
72
72
|
const legacy = await readLocalFile(legacySkillPath(root, slug));
|
|
73
|
-
if (legacy !== null &&
|
|
74
|
-
return
|
|
73
|
+
if (legacy !== null && legacy.length === 0)
|
|
74
|
+
return null;
|
|
75
|
+
if (legacy !== null)
|
|
76
|
+
return { hash: packageHash(legacy.toString("utf8"), []), source: "legacy" };
|
|
75
77
|
return null;
|
|
76
78
|
}
|
|
77
79
|
const localFiles = [];
|
|
@@ -81,7 +83,7 @@ async function localPackageHash(root, slug, target, files) {
|
|
|
81
83
|
return null;
|
|
82
84
|
localFiles.push({ path: file.path, bytes, sha256: file.sha256 });
|
|
83
85
|
}
|
|
84
|
-
return packageHash(main.toString("utf8"), localFiles);
|
|
86
|
+
return { hash: packageHash(main.toString("utf8"), localFiles), source: "native" };
|
|
85
87
|
}
|
|
86
88
|
async function markInstallSynced(root, slug, files) {
|
|
87
89
|
const manifest = await readSyncManifest();
|
|
@@ -119,6 +121,33 @@ async function writeInstallFile(root, target, body) {
|
|
|
119
121
|
await parent.close();
|
|
120
122
|
}
|
|
121
123
|
}
|
|
124
|
+
async function removeLegacySkillFile(root, slug) {
|
|
125
|
+
try {
|
|
126
|
+
await unlink(legacySkillPath(root, slug));
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
const code = err.code;
|
|
130
|
+
if (code === "ENOENT")
|
|
131
|
+
return;
|
|
132
|
+
if (code === "EISDIR" || code === "ELOOP" || code === "EPERM")
|
|
133
|
+
return;
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function removeEmptyNativeSkillDir(root, slug) {
|
|
138
|
+
try {
|
|
139
|
+
const dir = dirname(skillPath(root, slug));
|
|
140
|
+
const entries = await readdir(dir);
|
|
141
|
+
if (entries.length === 0)
|
|
142
|
+
await rmdir(dir);
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
const code = err.code;
|
|
146
|
+
if (code === "ENOENT" || code === "ENOTEMPTY" || code === "EEXIST")
|
|
147
|
+
return;
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
122
151
|
async function overwriteInstallFile(root, target, body) {
|
|
123
152
|
const parent = await openSafeParentDirectory(root, target);
|
|
124
153
|
const handle = await open(childCreatePath(parent, dirname(target), basename(target)), constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW, 0o600);
|
|
@@ -203,7 +232,7 @@ export async function install(slugInput, opts = {}) {
|
|
|
203
232
|
}
|
|
204
233
|
const cfg = await readConfig();
|
|
205
234
|
const apiUrl = resolveApiUrl(cfg);
|
|
206
|
-
const spinner = ora({ text: c.dim(`Adding ${slug}...`), color: "yellow" }).start();
|
|
235
|
+
const spinner = opts.json ? null : ora({ text: c.dim(`Adding ${slug}...`), color: "yellow" }).start();
|
|
207
236
|
let detail;
|
|
208
237
|
try {
|
|
209
238
|
detail = await getJson(`${apiUrl}/api/v1/skills/${encodeURIComponent(slug)}`, "fetch skill", cfg?.accessToken);
|
|
@@ -212,7 +241,7 @@ export async function install(slugInput, opts = {}) {
|
|
|
212
241
|
}
|
|
213
242
|
}
|
|
214
243
|
catch (err) {
|
|
215
|
-
spinner
|
|
244
|
+
spinner?.stop();
|
|
216
245
|
throw err;
|
|
217
246
|
}
|
|
218
247
|
const target = skillPath(root, slug);
|
|
@@ -238,21 +267,29 @@ export async function install(slugInput, opts = {}) {
|
|
|
238
267
|
}
|
|
239
268
|
throw err;
|
|
240
269
|
}
|
|
241
|
-
await
|
|
242
|
-
const existing = await localPackageHash(root, slug, target, remotePackageFiles);
|
|
270
|
+
const existing = await localPackageState(root, slug, target, remotePackageFiles);
|
|
243
271
|
const conflictingTarget = await preflightInstallPackage(root, installFiles, opts.force ? { force: true } : {});
|
|
244
272
|
if (conflictingTarget) {
|
|
245
273
|
throw new FloomError("Local skill already exists with different content.", `Run \`npx -y @floomhq/floom add <link> --force\` to replace it, or move the local file first: ${relative(root, conflictingTarget).split(sep).join("/")}`);
|
|
246
274
|
}
|
|
247
|
-
if (existing === remoteHash) {
|
|
275
|
+
if (existing?.hash === remoteHash && existing.source === "native") {
|
|
276
|
+
await removeLegacySkillFile(root, slug);
|
|
248
277
|
action = "unchanged";
|
|
249
278
|
}
|
|
250
|
-
else if (existing !== null && opts.force) {
|
|
279
|
+
else if (existing !== null && (opts.force || (existing.source === "legacy" && existing.hash === remoteHash))) {
|
|
251
280
|
try {
|
|
252
|
-
|
|
281
|
+
if (existing.source === "legacy")
|
|
282
|
+
await writeInstallFile(root, target, detail.body_md);
|
|
283
|
+
else
|
|
284
|
+
await overwriteInstallFile(root, target, detail.body_md);
|
|
253
285
|
for (const file of remotePackageFiles) {
|
|
254
|
-
|
|
286
|
+
const fileTarget = join(dirname(target), file.path);
|
|
287
|
+
if (existing.source === "legacy")
|
|
288
|
+
await writeInstallFile(root, fileTarget, file.bytes);
|
|
289
|
+
else
|
|
290
|
+
await overwriteInstallFile(root, fileTarget, file.bytes);
|
|
255
291
|
}
|
|
292
|
+
await removeLegacySkillFile(root, slug);
|
|
256
293
|
}
|
|
257
294
|
catch (err) {
|
|
258
295
|
const code = err.code;
|
|
@@ -260,7 +297,7 @@ export async function install(slugInput, opts = {}) {
|
|
|
260
297
|
throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
|
|
261
298
|
throw err;
|
|
262
299
|
}
|
|
263
|
-
action = "updated";
|
|
300
|
+
action = existing.source === "legacy" ? "installed" : "updated";
|
|
264
301
|
}
|
|
265
302
|
else if (existing !== null) {
|
|
266
303
|
throw new FloomError("Local skill already exists with different content.", "Run `npx -y @floomhq/floom add <link> --force` to replace it, or move the local file first.");
|
|
@@ -271,6 +308,7 @@ export async function install(slugInput, opts = {}) {
|
|
|
271
308
|
for (const file of remotePackageFiles) {
|
|
272
309
|
await writeInstallFile(root, join(dirname(target), file.path), file.bytes);
|
|
273
310
|
}
|
|
311
|
+
await removeLegacySkillFile(root, slug);
|
|
274
312
|
}
|
|
275
313
|
catch (err) {
|
|
276
314
|
const code = err.code;
|
|
@@ -294,7 +332,17 @@ export async function install(slugInput, opts = {}) {
|
|
|
294
332
|
manifestWarning = err instanceof Error ? err.message : String(err);
|
|
295
333
|
}
|
|
296
334
|
});
|
|
297
|
-
spinner
|
|
335
|
+
spinner?.stop();
|
|
336
|
+
if (opts.json) {
|
|
337
|
+
process.stdout.write(`${JSON.stringify({
|
|
338
|
+
slug,
|
|
339
|
+
action,
|
|
340
|
+
target: targetAgent,
|
|
341
|
+
path: target,
|
|
342
|
+
setup: Boolean(opts.setup),
|
|
343
|
+
})}\n`);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
298
346
|
process.stdout.write(`\n${symbols.ok} [floom] ${action} ${c.bold(slug)}\n`);
|
|
299
347
|
process.stdout.write(` ${c.dim(dirname(target))}\n\n`);
|
|
300
348
|
if (manifestWarning) {
|
package/dist/login.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { createServer as createNetServer } from "node:net";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
3
4
|
import open from "open";
|
|
4
5
|
import ora from "ora";
|
|
5
6
|
import { getApiUrl, writeConfig } from "./config.js";
|
|
@@ -92,6 +93,7 @@ function reserveEphemeralPort() {
|
|
|
92
93
|
function waitForCallback(port) {
|
|
93
94
|
return new Promise((resolve, reject) => {
|
|
94
95
|
const apiUrl = getApiUrl();
|
|
96
|
+
const state = randomBytes(32).toString("base64url");
|
|
95
97
|
let settled = false;
|
|
96
98
|
const server = createServer((req, res) => {
|
|
97
99
|
// CORS preflight from the browser bridge page.
|
|
@@ -122,6 +124,15 @@ function waitForCallback(port) {
|
|
|
122
124
|
res.end(localCallbackPage("Missing tokens from OAuth response."));
|
|
123
125
|
return;
|
|
124
126
|
}
|
|
127
|
+
if (data.state !== state) {
|
|
128
|
+
res.writeHead(400, {
|
|
129
|
+
"access-control-allow-origin": origin,
|
|
130
|
+
"access-control-allow-private-network": "true",
|
|
131
|
+
"content-type": "text/html; charset=utf-8",
|
|
132
|
+
});
|
|
133
|
+
res.end(localCallbackPage("Invalid sign-in state."));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
125
136
|
res.writeHead(200, {
|
|
126
137
|
"access-control-allow-origin": origin,
|
|
127
138
|
"access-control-allow-private-network": "true",
|
|
@@ -161,7 +172,7 @@ function waitForCallback(port) {
|
|
|
161
172
|
reject(new FloomError(`Local auth server failed on port ${port}.`, `Is port ${port} already in use? (${err.message})`));
|
|
162
173
|
});
|
|
163
174
|
server.listen(port, "127.0.0.1", () => {
|
|
164
|
-
const target = `${apiUrl}/auth/cli?port=${port}`;
|
|
175
|
+
const target = `${apiUrl}/auth/cli?port=${port}&state=${encodeURIComponent(state)}`;
|
|
165
176
|
open(target).catch((e) => {
|
|
166
177
|
const msg = e instanceof Error ? e.message : String(e);
|
|
167
178
|
process.stdout.write(c.yellow(`Could not auto-open browser (${msg}).\n`) +
|
|
@@ -175,7 +186,7 @@ function parseCallbackBody(body, contentType) {
|
|
|
175
186
|
if (type.includes("application/x-www-form-urlencoded")) {
|
|
176
187
|
const params = new URLSearchParams(body);
|
|
177
188
|
const parsed = {};
|
|
178
|
-
for (const key of ["access_token", "refresh_token", "expires_in", "token_type"]) {
|
|
189
|
+
for (const key of ["access_token", "refresh_token", "expires_in", "token_type", "state"]) {
|
|
179
190
|
const value = params.get(key);
|
|
180
191
|
if (value)
|
|
181
192
|
parsed[key] = value;
|
package/dist/sync.js
CHANGED
|
@@ -79,6 +79,17 @@ function skillPath(skill, targetAgent) {
|
|
|
79
79
|
function syncKey(skill) {
|
|
80
80
|
return `${skill.library_slug ?? ""}\0${skill.folder ?? ""}\0${skill.slug}`;
|
|
81
81
|
}
|
|
82
|
+
function hasStructuredPath(skill) {
|
|
83
|
+
return Boolean(skill.library_slug || skill.folder);
|
|
84
|
+
}
|
|
85
|
+
function dedupeSyncSkills(skills) {
|
|
86
|
+
const structuredSlugs = new Set();
|
|
87
|
+
for (const skill of skills) {
|
|
88
|
+
if (hasStructuredPath(skill))
|
|
89
|
+
structuredSlugs.add(skill.slug);
|
|
90
|
+
}
|
|
91
|
+
return skills.filter((skill) => !structuredSlugs.has(skill.slug) || hasStructuredPath(skill));
|
|
92
|
+
}
|
|
82
93
|
function validateSyncSkillShape(skill) {
|
|
83
94
|
if (!skill || typeof skill !== "object")
|
|
84
95
|
throw new FloomError("Invalid sync response.");
|
|
@@ -288,7 +299,7 @@ export async function sync(opts = {}) {
|
|
|
288
299
|
for (const skill of payload.skills)
|
|
289
300
|
validateSyncSkillShape(skill);
|
|
290
301
|
// Version 1 preview syncs published, saved, and subscribed library skills.
|
|
291
|
-
const all = payload.skills;
|
|
302
|
+
const all = dedupeSyncSkills(payload.skills);
|
|
292
303
|
const seen = new Set();
|
|
293
304
|
let unchanged = 0;
|
|
294
305
|
let updated = 0;
|
package/dist/targets.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export function resolveSkillsDir(target) {
|
|
4
|
+
if (process.env.FLOOM_SKILLS_DIR)
|
|
5
|
+
return process.env.FLOOM_SKILLS_DIR;
|
|
6
|
+
if (target === "codex") {
|
|
7
|
+
const codexHome = process.env.CODEX_HOME ?? join(homedir(), ".codex");
|
|
8
|
+
return process.env.CODEX_SKILLS_DIR ?? join(codexHome, "skills");
|
|
9
|
+
}
|
|
10
|
+
return process.env.CLAUDE_SKILLS_DIR ?? join(homedir(), ".claude", "skills");
|
|
11
|
+
}
|
|
12
|
+
export function skillsDirHint(target) {
|
|
13
|
+
if (process.env.FLOOM_SKILLS_DIR)
|
|
14
|
+
return "FLOOM_SKILLS_DIR";
|
|
15
|
+
return target === "codex" ? "CODEX_SKILLS_DIR" : "CLAUDE_SKILLS_DIR";
|
|
16
|
+
}
|