@floomhq/floom 3.0.2 → 3.0.4
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 +168 -0
- package/dist/index.js +365 -227
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3067,6 +3067,33 @@ init_src();
|
|
|
3067
3067
|
import { homedir as homedir2 } from "node:os";
|
|
3068
3068
|
import { join as join3 } from "node:path";
|
|
3069
3069
|
import { mkdir as mkdir2, readFile as readFile3, writeFile, chmod } from "node:fs/promises";
|
|
3070
|
+
|
|
3071
|
+
// src/lib/invocation.ts
|
|
3072
|
+
var NPX_HINTS = ["_npx", "npm-cache/_npx", "npm-cache\\_npx", ".npm/_npx"];
|
|
3073
|
+
var NPX_INVOCATION = "npx -y @floomhq/floom";
|
|
3074
|
+
var GLOBAL_INVOCATION = "floom";
|
|
3075
|
+
var cached = null;
|
|
3076
|
+
function detect() {
|
|
3077
|
+
const override = process.env.FLOOM_INVOKED_AS;
|
|
3078
|
+
if (override && (override === NPX_INVOCATION || override === GLOBAL_INVOCATION)) {
|
|
3079
|
+
return override;
|
|
3080
|
+
}
|
|
3081
|
+
const argv1 = process.argv[1] ?? "";
|
|
3082
|
+
for (const hint of NPX_HINTS) {
|
|
3083
|
+
if (argv1.includes(hint)) return NPX_INVOCATION;
|
|
3084
|
+
}
|
|
3085
|
+
return GLOBAL_INVOCATION;
|
|
3086
|
+
}
|
|
3087
|
+
function cliInvocation() {
|
|
3088
|
+
if (cached === null) cached = detect();
|
|
3089
|
+
return cached;
|
|
3090
|
+
}
|
|
3091
|
+
function cliCmd(cmd) {
|
|
3092
|
+
const stripped = cmd.startsWith("floom ") ? cmd.slice("floom ".length) : cmd;
|
|
3093
|
+
return `${cliInvocation()} ${stripped}`;
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
// src/config.ts
|
|
3070
3097
|
var CONFIG_DIR = join3(homedir2(), ".floom");
|
|
3071
3098
|
var AUTH_FILE = join3(CONFIG_DIR, "auth.json");
|
|
3072
3099
|
var DEFAULT_APP_URL = "https://floom.dev";
|
|
@@ -3119,18 +3146,25 @@ async function readRawAuth() {
|
|
|
3119
3146
|
if (code === "ENOENT") return null;
|
|
3120
3147
|
if (code === "EACCES" || code === "EPERM") {
|
|
3121
3148
|
throw new Error(
|
|
3122
|
-
|
|
3149
|
+
`Cannot read your Floom auth file (permission denied).
|
|
3150
|
+
Fix: chmod 600 ~/.floom/auth.json
|
|
3151
|
+
Or re-authenticate: ${cliCmd("login")}
|
|
3152
|
+
More help: https://floom.dev/docs#troubleshooting`
|
|
3123
3153
|
);
|
|
3124
3154
|
}
|
|
3125
3155
|
throw new Error(
|
|
3126
|
-
|
|
3156
|
+
`Cannot read your Floom auth file.
|
|
3157
|
+
Run: ${cliCmd("login")}
|
|
3158
|
+
More help: https://floom.dev/docs#troubleshooting`
|
|
3127
3159
|
);
|
|
3128
3160
|
}
|
|
3129
3161
|
try {
|
|
3130
3162
|
return JSON.parse(raw);
|
|
3131
3163
|
} catch {
|
|
3132
3164
|
throw new Error(
|
|
3133
|
-
|
|
3165
|
+
`Your Floom auth file is corrupted.
|
|
3166
|
+
Run: ${cliCmd("login")} to generate a fresh one.
|
|
3167
|
+
More help: https://floom.dev/docs#troubleshooting`
|
|
3134
3168
|
);
|
|
3135
3169
|
}
|
|
3136
3170
|
}
|
|
@@ -3253,7 +3287,7 @@ async function getMachineIdentity() {
|
|
|
3253
3287
|
}
|
|
3254
3288
|
|
|
3255
3289
|
// src/version.ts
|
|
3256
|
-
var VERSION = "3.0.
|
|
3290
|
+
var VERSION = "3.0.4";
|
|
3257
3291
|
|
|
3258
3292
|
// src/api-client.ts
|
|
3259
3293
|
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
@@ -3304,12 +3338,12 @@ var HELP = "https://floom.dev/docs#troubleshooting";
|
|
|
3304
3338
|
function friendlyApiErrorMessage(code, raw, status) {
|
|
3305
3339
|
if (code === "AUTH_REQUIRED" || code === "TOKEN_INVALID" || status === 401) {
|
|
3306
3340
|
return `Not signed in.
|
|
3307
|
-
Run:
|
|
3341
|
+
Run: ${cliCmd("login")}
|
|
3308
3342
|
More help: ${HELP}`;
|
|
3309
3343
|
}
|
|
3310
3344
|
if (code === "TOKEN_EXPIRED") {
|
|
3311
3345
|
return `Your session has expired.
|
|
3312
|
-
Run:
|
|
3346
|
+
Run: ${cliCmd("login")}
|
|
3313
3347
|
More help: ${HELP}`;
|
|
3314
3348
|
}
|
|
3315
3349
|
if (code === "SKILL_ACCESS_DENIED" || status === 403) {
|
|
@@ -3318,7 +3352,7 @@ function friendlyApiErrorMessage(code, raw, status) {
|
|
|
3318
3352
|
}
|
|
3319
3353
|
if (code === "SKILL_NOT_FOUND" || status === 404) {
|
|
3320
3354
|
return `Skill not found. It may have been deleted or the slug is wrong.
|
|
3321
|
-
Run:
|
|
3355
|
+
Run: ${cliCmd("list")}
|
|
3322
3356
|
More help: ${HELP}`;
|
|
3323
3357
|
}
|
|
3324
3358
|
if (code === "RATE_LIMITED" || status === 429) {
|
|
@@ -3346,7 +3380,9 @@ async function api(path, opts = {}) {
|
|
|
3346
3380
|
if (opts.authRequired && !token) {
|
|
3347
3381
|
throw new FloomError(
|
|
3348
3382
|
"AUTH_REQUIRED",
|
|
3349
|
-
|
|
3383
|
+
`Not signed in.
|
|
3384
|
+
Run: ${cliCmd("login")}
|
|
3385
|
+
More help: https://floom.dev/docs#troubleshooting`
|
|
3350
3386
|
);
|
|
3351
3387
|
}
|
|
3352
3388
|
let lastError = null;
|
|
@@ -3425,7 +3461,7 @@ async function api(path, opts = {}) {
|
|
|
3425
3461
|
// src/lib/agents.ts
|
|
3426
3462
|
init_src();
|
|
3427
3463
|
import { createHash as createHash3 } from "node:crypto";
|
|
3428
|
-
import { readdir as readdir2, readFile as readFile5, stat as stat3, lstat as lstat2 } from "node:fs/promises";
|
|
3464
|
+
import { readdir as readdir2, readFile as readFile5, readlink, stat as stat3, lstat as lstat2 } from "node:fs/promises";
|
|
3429
3465
|
import { homedir as homedir4 } from "node:os";
|
|
3430
3466
|
import { join as join5, posix as posix2, relative as relative3, resolve as resolve2, sep as sep3 } from "node:path";
|
|
3431
3467
|
var AGENT_LABELS = {
|
|
@@ -3550,7 +3586,28 @@ async function collectHashFiles(root) {
|
|
|
3550
3586
|
const abs = join5(dir, entry.name);
|
|
3551
3587
|
const info = await lstat2(abs);
|
|
3552
3588
|
if (info.isSymbolicLink()) {
|
|
3553
|
-
|
|
3589
|
+
let target = "(unreadable)";
|
|
3590
|
+
try {
|
|
3591
|
+
target = await readlink(abs);
|
|
3592
|
+
} catch {
|
|
3593
|
+
}
|
|
3594
|
+
throw new Error(
|
|
3595
|
+
`Skill folder contains a symlink (not allowed for security):
|
|
3596
|
+
link: ${abs}
|
|
3597
|
+
target: ${target}
|
|
3598
|
+
|
|
3599
|
+
Why: a symlink could point outside the skill folder and quietly leak files
|
|
3600
|
+
(SSH keys, .env, cookie jars) into a public Library. Floom refuses to read
|
|
3601
|
+
through symlinks for that reason.
|
|
3602
|
+
|
|
3603
|
+
Fix: replace the link with the real file contents:
|
|
3604
|
+
rm "${abs}"
|
|
3605
|
+
cp "${target}" "${abs}"
|
|
3606
|
+
|
|
3607
|
+
(or move the target file under the skill folder if it lives elsewhere)
|
|
3608
|
+
|
|
3609
|
+
More help: https://floom.dev/docs#skill-folder-rules`
|
|
3610
|
+
);
|
|
3554
3611
|
}
|
|
3555
3612
|
if (entry.isDirectory()) {
|
|
3556
3613
|
await walk2(abs, childRel);
|
|
@@ -3622,8 +3679,16 @@ function tildePath(path, homeDir = homedir4()) {
|
|
|
3622
3679
|
}
|
|
3623
3680
|
|
|
3624
3681
|
// src/commands/login.ts
|
|
3682
|
+
function isHeadlessLinuxSession(env = process.env, platform3 = process.platform) {
|
|
3683
|
+
if (env.FLOOM_NO_OPEN === "1") return true;
|
|
3684
|
+
if (platform3 !== "linux") return false;
|
|
3685
|
+
const isSsh = Boolean(env.SSH_CONNECTION ?? env.SSH_CLIENT ?? env.SSH_TTY);
|
|
3686
|
+
const hasDisplay = Boolean(env.DISPLAY ?? env.WAYLAND_DISPLAY);
|
|
3687
|
+
return isSsh || !hasDisplay;
|
|
3688
|
+
}
|
|
3625
3689
|
function tryOpenBrowser(url) {
|
|
3626
3690
|
if (process.env.FLOOM_NO_OPEN === "1") return;
|
|
3691
|
+
if (isHeadlessLinuxSession()) return;
|
|
3627
3692
|
const platform3 = process.platform;
|
|
3628
3693
|
let cmd;
|
|
3629
3694
|
let args;
|
|
@@ -3666,8 +3731,17 @@ async function loginCommand() {
|
|
|
3666
3731
|
});
|
|
3667
3732
|
log.heading("Sign in to Floom");
|
|
3668
3733
|
log.blank();
|
|
3669
|
-
|
|
3670
|
-
|
|
3734
|
+
const headless = isHeadlessLinuxSession();
|
|
3735
|
+
if (headless) {
|
|
3736
|
+
log.info(" Open this URL in any browser to approve:");
|
|
3737
|
+
log.info(` ${session.verification_uri}`);
|
|
3738
|
+
log.info(` Code: ${chalk2.bold.cyan(session.user_code)}`);
|
|
3739
|
+
log.blank();
|
|
3740
|
+
log.info(" You can open it on your phone or laptop \u2014 the same URL works from anywhere.");
|
|
3741
|
+
} else {
|
|
3742
|
+
log.info(` Open: ${session.verification_uri}`);
|
|
3743
|
+
log.info(` Code: ${chalk2.bold.cyan(session.user_code)}`);
|
|
3744
|
+
}
|
|
3671
3745
|
log.blank();
|
|
3672
3746
|
process.stdout.write("Waiting for browser approval...");
|
|
3673
3747
|
tryOpenBrowser(session.verification_uri);
|
|
@@ -3693,9 +3767,13 @@ async function loginCommand() {
|
|
|
3693
3767
|
if (token.workspace?.name) log.info(` Workspace: ${token.workspace.name}`);
|
|
3694
3768
|
await printAgentsFound();
|
|
3695
3769
|
printNext([
|
|
3696
|
-
{ command: "
|
|
3697
|
-
{ command: "
|
|
3770
|
+
{ command: cliCmd("pull"), description: "install your team's skills into these agents" },
|
|
3771
|
+
{ command: cliCmd("push"), description: "if you have skills here to publish first" }
|
|
3698
3772
|
]);
|
|
3773
|
+
if (cliCmd("login").startsWith("npx ")) {
|
|
3774
|
+
log.blank();
|
|
3775
|
+
log.info(" Tip: install once with npm i -g @floomhq/floom and just type `floom` after that.");
|
|
3776
|
+
}
|
|
3699
3777
|
return;
|
|
3700
3778
|
}
|
|
3701
3779
|
} catch (error) {
|
|
@@ -3714,27 +3792,45 @@ async function loginCommand() {
|
|
|
3714
3792
|
process.stdout.write("\n");
|
|
3715
3793
|
log.err("Login timed out \u2014 the browser window expired.");
|
|
3716
3794
|
printNext([
|
|
3717
|
-
{ command: "
|
|
3795
|
+
{ command: cliCmd("login"), description: "try again" },
|
|
3718
3796
|
{ command: "More help: https://floom.dev/docs#troubleshooting" }
|
|
3719
3797
|
]);
|
|
3720
3798
|
process.exitCode = 1;
|
|
3721
3799
|
}
|
|
3722
3800
|
|
|
3723
3801
|
// src/commands/logout.ts
|
|
3724
|
-
async function logoutCommand() {
|
|
3725
|
-
const
|
|
3802
|
+
async function logoutCommand(opts = {}, deps = {}) {
|
|
3803
|
+
const readAuth2 = deps.readAuth ?? readAuth;
|
|
3804
|
+
const clearAuth2 = deps.clearAuth ?? clearAuth;
|
|
3805
|
+
const api2 = deps.api ?? api;
|
|
3806
|
+
const auth = await readAuth2();
|
|
3807
|
+
let remoteRevokeFailed = false;
|
|
3726
3808
|
if (auth) {
|
|
3727
3809
|
try {
|
|
3728
|
-
await
|
|
3810
|
+
await api2("/cli/session/revoke", {
|
|
3811
|
+
method: "POST",
|
|
3812
|
+
authRequired: true,
|
|
3813
|
+
query: opts.all ? { scope: "all" } : void 0
|
|
3814
|
+
});
|
|
3729
3815
|
} catch (error) {
|
|
3730
|
-
|
|
3816
|
+
if (opts.all) {
|
|
3817
|
+
remoteRevokeFailed = true;
|
|
3818
|
+
process.exitCode = 1;
|
|
3819
|
+
log.err(
|
|
3820
|
+
"Local session cleared. Other devices are still authenticated.\nSign in at https://floom.dev/settings to revoke other sessions from the web."
|
|
3821
|
+
);
|
|
3822
|
+
} else {
|
|
3823
|
+
log.warn(`Remote session revoke failed: ${error.message}`);
|
|
3824
|
+
}
|
|
3731
3825
|
}
|
|
3732
3826
|
}
|
|
3733
|
-
await
|
|
3734
|
-
log.ok(
|
|
3827
|
+
await clearAuth2();
|
|
3828
|
+
log.ok(
|
|
3829
|
+
remoteRevokeFailed ? "Local sign-out only \u2014 remote revoke failed." : opts.all ? "Signed out of Floom CLI on all machines." : "Signed out of Floom on this machine."
|
|
3830
|
+
);
|
|
3735
3831
|
log.blank();
|
|
3736
3832
|
log.info("Your local skill files in ~/.claude/skills, ~/.codex/skills, etc. were not changed.");
|
|
3737
|
-
printNext([{ command: "
|
|
3833
|
+
printNext([{ command: cliCmd("login"), description: "sign in again" }]);
|
|
3738
3834
|
}
|
|
3739
3835
|
|
|
3740
3836
|
// src/commands/account.ts
|
|
@@ -3789,18 +3885,18 @@ async function accountCommand(opts = {}) {
|
|
|
3789
3885
|
const device = await deviceLabel();
|
|
3790
3886
|
if (!auth) {
|
|
3791
3887
|
if (json) {
|
|
3792
|
-
emitJson({ signedIn: false, email: null, workspace: null, device: device ? { label: device } : null, next: ["
|
|
3888
|
+
emitJson({ signedIn: false, email: null, workspace: null, device: device ? { label: device } : null, next: [cliCmd("login")] });
|
|
3793
3889
|
return;
|
|
3794
3890
|
}
|
|
3795
3891
|
log.info("Not signed in to Floom.");
|
|
3796
|
-
printNext([{ command: "
|
|
3892
|
+
printNext([{ command: cliCmd("login") }]);
|
|
3797
3893
|
return;
|
|
3798
3894
|
}
|
|
3799
3895
|
if (!json) {
|
|
3800
3896
|
const rawAuth = await readRawAuth();
|
|
3801
3897
|
if (rawAuth && !isTrustedApiUrl(rawAuth.apiUrl)) {
|
|
3802
3898
|
log.warn(
|
|
3803
|
-
`Your saved Floom credentials point at ${rawAuth.apiUrl}, which is not a trusted Floom API. Run '
|
|
3899
|
+
`Your saved Floom credentials point at ${rawAuth.apiUrl}, which is not a trusted Floom API. Run '${cliCmd("login")}' to re-authenticate.`
|
|
3804
3900
|
);
|
|
3805
3901
|
}
|
|
3806
3902
|
}
|
|
@@ -3812,7 +3908,7 @@ async function accountCommand(opts = {}) {
|
|
|
3812
3908
|
email: me.user.email ?? auth.email,
|
|
3813
3909
|
workspace: me.workspace ? { name: me.workspace.name } : null,
|
|
3814
3910
|
device: { label: device },
|
|
3815
|
-
next: ["
|
|
3911
|
+
next: [cliCmd("status"), cliCmd("logout")]
|
|
3816
3912
|
});
|
|
3817
3913
|
return;
|
|
3818
3914
|
}
|
|
@@ -3822,19 +3918,19 @@ async function accountCommand(opts = {}) {
|
|
|
3822
3918
|
if (me.workspace) log.kv("Workspace:", me.workspace.name);
|
|
3823
3919
|
log.kv("This machine:", device ?? "not named yet");
|
|
3824
3920
|
printNext([
|
|
3825
|
-
{ command: "
|
|
3826
|
-
{ command: "
|
|
3921
|
+
{ command: cliCmd("status"), description: "see Library and local agent copies" },
|
|
3922
|
+
{ command: cliCmd("logout"), description: "sign out on this machine" }
|
|
3827
3923
|
]);
|
|
3828
3924
|
} catch (error) {
|
|
3829
3925
|
if (json) {
|
|
3830
|
-
emitJson({ signedIn: false, email: auth.email, workspace: null, device: { label: device }, next: ["
|
|
3926
|
+
emitJson({ signedIn: false, email: auth.email, workspace: null, device: { label: device }, next: [cliCmd("login")] });
|
|
3831
3927
|
process.exitCode = 1;
|
|
3832
3928
|
return;
|
|
3833
3929
|
}
|
|
3834
3930
|
log.err(error.message);
|
|
3835
3931
|
log.blank();
|
|
3836
3932
|
log.info("Session expired. Sign in again to continue.");
|
|
3837
|
-
printNext([{ command: "
|
|
3933
|
+
printNext([{ command: cliCmd("login") }]);
|
|
3838
3934
|
process.exitCode = 1;
|
|
3839
3935
|
}
|
|
3840
3936
|
void isJsonMode;
|
|
@@ -4702,7 +4798,7 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
|
|
|
4702
4798
|
syncedCount: 0,
|
|
4703
4799
|
plan: [],
|
|
4704
4800
|
error: { code: "AUTH_REQUIRED", message: "Not signed in \u2014 log in first to publish." },
|
|
4705
|
-
next: ["
|
|
4801
|
+
next: [cliCmd("login")]
|
|
4706
4802
|
});
|
|
4707
4803
|
process.exitCode = 1;
|
|
4708
4804
|
return;
|
|
@@ -4712,7 +4808,7 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
|
|
|
4712
4808
|
log.blank();
|
|
4713
4809
|
if (skills.length === 0) {
|
|
4714
4810
|
log.info("No local skill folders found on this machine.");
|
|
4715
|
-
printNext([{ command: "
|
|
4811
|
+
printNext([{ command: cliCmd("login") }]);
|
|
4716
4812
|
process.exitCode = 1;
|
|
4717
4813
|
return;
|
|
4718
4814
|
}
|
|
@@ -4720,10 +4816,10 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
|
|
|
4720
4816
|
for (const skill of skills) log.info(` ${tildePath(skill.path)}`);
|
|
4721
4817
|
log.blank();
|
|
4722
4818
|
log.info("To compare and publish them, sign in to Floom.");
|
|
4723
|
-
printNext([{ command: "
|
|
4819
|
+
printNext([{ command: cliCmd("login") }]);
|
|
4724
4820
|
log.blank();
|
|
4725
4821
|
log.info("After login, run:");
|
|
4726
|
-
log.command("
|
|
4822
|
+
log.command(cliCmd("push"));
|
|
4727
4823
|
process.exitCode = 1;
|
|
4728
4824
|
return;
|
|
4729
4825
|
}
|
|
@@ -4741,7 +4837,7 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
|
|
|
4741
4837
|
wouldMutate: false,
|
|
4742
4838
|
syncedCount: 0,
|
|
4743
4839
|
plan: [],
|
|
4744
|
-
next: libCount > 0 ? ["
|
|
4840
|
+
next: libCount > 0 ? [cliCmd("pull")] : [cliCmd("new")]
|
|
4745
4841
|
});
|
|
4746
4842
|
return;
|
|
4747
4843
|
}
|
|
@@ -4750,11 +4846,11 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
|
|
|
4750
4846
|
log.blank();
|
|
4751
4847
|
if (libCount > 0) {
|
|
4752
4848
|
log.info(`Your Library has ${libCount} skill${libCount === 1 ? "" : "s"}. To install them here, run:`);
|
|
4753
|
-
log.command("
|
|
4849
|
+
log.command(cliCmd("pull"));
|
|
4754
4850
|
}
|
|
4755
4851
|
printNext([
|
|
4756
|
-
{ command: "
|
|
4757
|
-
{ command: "
|
|
4852
|
+
{ command: cliCmd("pull"), description: "install your Library on this machine" },
|
|
4853
|
+
{ command: cliCmd("new research-helper"), description: "create a ready-to-edit skill" }
|
|
4758
4854
|
]);
|
|
4759
4855
|
return;
|
|
4760
4856
|
}
|
|
@@ -4797,7 +4893,7 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
|
|
|
4797
4893
|
lastSyncedLibraryVersion: r.lastSyncedLibraryVersion,
|
|
4798
4894
|
publishVersion: r.publishVersion
|
|
4799
4895
|
})),
|
|
4800
|
-
next: ["
|
|
4896
|
+
next: [cliCmd("push --yes"), cliCmd("push")]
|
|
4801
4897
|
});
|
|
4802
4898
|
process.exitCode = wouldMutate && !options.exitZero ? 1 : 0;
|
|
4803
4899
|
return;
|
|
@@ -4819,7 +4915,7 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
|
|
|
4819
4915
|
if (!wouldMutate && refusedRows.length === 0) {
|
|
4820
4916
|
log.blank();
|
|
4821
4917
|
log.info("Everything is already in sync with the Library. Nothing to publish.");
|
|
4822
|
-
printNext([{ command: "
|
|
4918
|
+
printNext([{ command: cliCmd("status"), description: "see every copy" }]);
|
|
4823
4919
|
return;
|
|
4824
4920
|
}
|
|
4825
4921
|
let toPublish = [];
|
|
@@ -4827,12 +4923,12 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
|
|
|
4827
4923
|
toPublish = safeRows;
|
|
4828
4924
|
} else if (!isInteractive()) {
|
|
4829
4925
|
log.blank();
|
|
4830
|
-
log.err("
|
|
4926
|
+
log.err(`${cliCmd("push")} needs a choice but is not running in an interactive terminal.`);
|
|
4831
4927
|
log.blank();
|
|
4832
4928
|
log.info("Re-run with:");
|
|
4833
|
-
log.command("
|
|
4834
|
-
log.command("
|
|
4835
|
-
log.command("
|
|
4929
|
+
log.command(cliCmd("push --dry-run") + " preview what would happen");
|
|
4930
|
+
log.command(cliCmd("push --yes") + " proceed with all safe changes");
|
|
4931
|
+
log.command(cliCmd("push --json") + " get the plan as JSON");
|
|
4836
4932
|
process.exitCode = 2;
|
|
4837
4933
|
return;
|
|
4838
4934
|
} else {
|
|
@@ -4882,9 +4978,9 @@ async function pushCommand(pathArg, options = {}, deps = {}) {
|
|
|
4882
4978
|
}
|
|
4883
4979
|
const conflictSlug = skipped.find((r) => r.action === "conflict")?.slug;
|
|
4884
4980
|
printNext([
|
|
4885
|
-
...conflictSlug ? [{ command: `
|
|
4886
|
-
{ command: "
|
|
4887
|
-
{ command: "
|
|
4981
|
+
...conflictSlug ? [{ command: cliCmd(`diff ${conflictSlug}`), description: "review the conflict before publishing" }] : [],
|
|
4982
|
+
{ command: cliCmd("pull"), description: "update your other agents with these new versions" },
|
|
4983
|
+
{ command: cliCmd("status"), description: "see every copy" }
|
|
4888
4984
|
]);
|
|
4889
4985
|
if (failures.length > 0 || skipped.length > 0) process.exitCode = 1;
|
|
4890
4986
|
} finally {
|
|
@@ -4897,10 +4993,10 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
|
|
|
4897
4993
|
const info = await stat5(folder).catch(() => null);
|
|
4898
4994
|
if (!info || !info.isDirectory()) {
|
|
4899
4995
|
if (json) {
|
|
4900
|
-
emitJson({ workspace: { name: "Library", signedIn: authed }, mode: "plan", applied: false, wouldMutate: false, syncedCount: 0, plan: [], next: ["
|
|
4996
|
+
emitJson({ workspace: { name: "Library", signedIn: authed }, mode: "plan", applied: false, wouldMutate: false, syncedCount: 0, plan: [], next: [cliCmd("push")] });
|
|
4901
4997
|
} else {
|
|
4902
4998
|
log.err(`Folder not found: ${pathArg}`);
|
|
4903
|
-
log.info(
|
|
4999
|
+
log.info(` Run ${cliCmd("push")} with no arguments to auto-detect your skills, or pass a valid folder path.`);
|
|
4904
5000
|
}
|
|
4905
5001
|
process.exitCode = 2;
|
|
4906
5002
|
return;
|
|
@@ -4923,13 +5019,13 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
|
|
|
4923
5019
|
syncedCount: 0,
|
|
4924
5020
|
plan: [],
|
|
4925
5021
|
error: { code: "AUTH_REQUIRED", message: "Not signed in \u2014 log in first to publish." },
|
|
4926
|
-
next: ["
|
|
5022
|
+
next: [cliCmd("login")]
|
|
4927
5023
|
});
|
|
4928
5024
|
} else {
|
|
4929
5025
|
log.blank();
|
|
4930
5026
|
log.err("Not signed in \u2014 log in first to publish.");
|
|
4931
5027
|
log.info(`Found local skill at ${tildePath(folder)}.`);
|
|
4932
|
-
printNext([{ command: "
|
|
5028
|
+
printNext([{ command: cliCmd("login") }]);
|
|
4933
5029
|
}
|
|
4934
5030
|
process.exitCode = 1;
|
|
4935
5031
|
return;
|
|
@@ -4946,7 +5042,7 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
|
|
|
4946
5042
|
wouldMutate: false,
|
|
4947
5043
|
syncedCount: 0,
|
|
4948
5044
|
plan: [{ slug, scope: "global", path: folder, action: "changed", status: "refused", refusedReason: "secret_scan_failed", message: `${slug} was refused because the secret scan could not run`, localHash: "", lastSyncedHash: null, libraryVersion: null, lastSyncedLibraryVersion: null, publishVersion: null }],
|
|
4949
|
-
next: ["
|
|
5045
|
+
next: [cliCmd("push --no-secret-check")]
|
|
4950
5046
|
});
|
|
4951
5047
|
process.exitCode = 1;
|
|
4952
5048
|
return;
|
|
@@ -4968,7 +5064,7 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
|
|
|
4968
5064
|
wouldMutate: false,
|
|
4969
5065
|
syncedCount: 0,
|
|
4970
5066
|
plan: [{ slug, scope: "global", path: folder, action: "changed", status: "refused", refusedReason: "secret_detected", message: `${slug} was refused because a file looks like it contains a secret`, localHash: "", lastSyncedHash: null, libraryVersion: null, lastSyncedLibraryVersion: null, publishVersion: null }],
|
|
4971
|
-
next: ["
|
|
5067
|
+
next: [cliCmd("push --no-secret-check")]
|
|
4972
5068
|
});
|
|
4973
5069
|
process.exitCode = 1;
|
|
4974
5070
|
return;
|
|
@@ -4993,7 +5089,7 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
|
|
|
4993
5089
|
log.info("Nothing published.");
|
|
4994
5090
|
printNext([
|
|
4995
5091
|
{ command: "Edit .floomignore to exclude secrets or generated files" },
|
|
4996
|
-
{ command: `
|
|
5092
|
+
{ command: cliCmd(`push ${pathArg}`) }
|
|
4997
5093
|
]);
|
|
4998
5094
|
return;
|
|
4999
5095
|
}
|
|
@@ -5008,7 +5104,7 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
|
|
|
5008
5104
|
wouldMutate: true,
|
|
5009
5105
|
syncedCount: 0,
|
|
5010
5106
|
plan: [{ slug, scope: "global", path: folder, action: "changed", status: "ok", refusedReason: null, message: `${slug} would be published`, localHash: await skillContentHash(folder) ?? "", lastSyncedHash: null, libraryVersion: null, lastSyncedLibraryVersion: null, publishVersion: null }],
|
|
5011
|
-
next: [
|
|
5107
|
+
next: [cliCmd(`push ${pathArg} --yes`)]
|
|
5012
5108
|
});
|
|
5013
5109
|
} else {
|
|
5014
5110
|
log.heading("Push plan");
|
|
@@ -5028,15 +5124,15 @@ async function pushExplicitPath(pathArg, options, pushApi, authed) {
|
|
|
5028
5124
|
mode: "apply",
|
|
5029
5125
|
applied: true,
|
|
5030
5126
|
results: [{ slug: result.skill.slug, scope: "global", path: folder, action: "changed", result: "published", message: `${result.skill.slug} published as ${result.skill.latest.display}`, libraryVersion: result.skill.latest.version_seq ?? null, publishVersion: result.skill.latest.version_seq ?? null, backupPath: null }],
|
|
5031
|
-
next: ["
|
|
5127
|
+
next: [cliCmd("pull")]
|
|
5032
5128
|
});
|
|
5033
5129
|
} else {
|
|
5034
5130
|
log.ok(`Published ${result.skill.slug} ${result.skill.latest.display}`);
|
|
5035
|
-
printNext([{ command: "
|
|
5131
|
+
printNext([{ command: cliCmd("pull"), description: `update your other agents with ${result.skill.slug}` }]);
|
|
5036
5132
|
}
|
|
5037
5133
|
} catch (error) {
|
|
5038
5134
|
if (json) {
|
|
5039
|
-
emitJson({ workspace: { name: "Library", signedIn: true }, mode: "apply", applied: true, results: [{ slug, scope: "global", path: folder, action: "changed", result: "failed", message: error.message, libraryVersion: null, publishVersion: null, backupPath: null }], next: [
|
|
5135
|
+
emitJson({ workspace: { name: "Library", signedIn: true }, mode: "apply", applied: true, results: [{ slug, scope: "global", path: folder, action: "changed", result: "failed", message: error.message, libraryVersion: null, publishVersion: null, backupPath: null }], next: [cliCmd(`push ${pathArg}`)] });
|
|
5040
5136
|
} else {
|
|
5041
5137
|
log.err(error.message);
|
|
5042
5138
|
}
|
|
@@ -5077,11 +5173,11 @@ async function deleteCommand(slug, opts = {}, deps = { api }) {
|
|
|
5077
5173
|
skill: { slug: "", version: null, exists: false },
|
|
5078
5174
|
wouldMutate: false,
|
|
5079
5175
|
action: "missing_argument",
|
|
5080
|
-
message: "
|
|
5081
|
-
next: ["
|
|
5176
|
+
message: `${cliCmd("delete")} needs a skill name.`,
|
|
5177
|
+
next: [cliCmd("list")]
|
|
5082
5178
|
});
|
|
5083
5179
|
} else {
|
|
5084
|
-
log.err("
|
|
5180
|
+
log.err(`${cliCmd("delete")} needs a skill name. Run \`${cliCmd("list")}\` to see skills in the Library.`);
|
|
5085
5181
|
}
|
|
5086
5182
|
process.exitCode = 2;
|
|
5087
5183
|
return;
|
|
@@ -5098,10 +5194,10 @@ async function deleteCommand(slug, opts = {}, deps = { api }) {
|
|
|
5098
5194
|
wouldMutate: false,
|
|
5099
5195
|
action: "not_found",
|
|
5100
5196
|
message: `No skill named '${slug}' in ${ws}.`,
|
|
5101
|
-
next: ["
|
|
5197
|
+
next: [cliCmd("list")]
|
|
5102
5198
|
});
|
|
5103
5199
|
} else {
|
|
5104
|
-
log.err(`No skill named '${slug}' in ${ws}. Run
|
|
5200
|
+
log.err(`No skill named '${slug}' in ${ws}. Run \`${cliCmd("list")}\` to see what is in the Library.`);
|
|
5105
5201
|
}
|
|
5106
5202
|
process.exitCode = 2;
|
|
5107
5203
|
return;
|
|
@@ -5116,7 +5212,7 @@ async function deleteCommand(slug, opts = {}, deps = { api }) {
|
|
|
5116
5212
|
wouldMutate: true,
|
|
5117
5213
|
action: "delete",
|
|
5118
5214
|
message: `${slug} would be deleted from the Library.`,
|
|
5119
|
-
next: [
|
|
5215
|
+
next: [cliCmd(`delete ${slug} --yes`)]
|
|
5120
5216
|
});
|
|
5121
5217
|
} else {
|
|
5122
5218
|
log.heading(`Delete plan for ${ws} Library`);
|
|
@@ -5142,17 +5238,17 @@ async function deleteCommand(slug, opts = {}, deps = { api }) {
|
|
|
5142
5238
|
wouldMutate: true,
|
|
5143
5239
|
action: "delete",
|
|
5144
5240
|
message: `${slug} would be deleted from the Library.`,
|
|
5145
|
-
next: [
|
|
5241
|
+
next: [cliCmd(`delete ${slug} --yes`)]
|
|
5146
5242
|
});
|
|
5147
5243
|
process.exitCode = opts.exitZero ? 0 : 1;
|
|
5148
5244
|
return;
|
|
5149
5245
|
}
|
|
5150
5246
|
if (!opts.yes) {
|
|
5151
5247
|
if (!isInteractive()) {
|
|
5152
|
-
log.err("
|
|
5248
|
+
log.err(`${cliCmd("delete")} needs confirmation but is not running in an interactive terminal.`);
|
|
5153
5249
|
log.blank();
|
|
5154
5250
|
log.info("Re-run with:");
|
|
5155
|
-
log.command(`
|
|
5251
|
+
log.command(cliCmd(`delete ${slug} --yes`));
|
|
5156
5252
|
process.exitCode = 2;
|
|
5157
5253
|
return;
|
|
5158
5254
|
}
|
|
@@ -5181,7 +5277,7 @@ async function deleteCommand(slug, opts = {}, deps = { api }) {
|
|
|
5181
5277
|
skill: { slug, version: info.versionSeq, exists: true },
|
|
5182
5278
|
result: "failed",
|
|
5183
5279
|
message: error.message,
|
|
5184
|
-
next: [
|
|
5280
|
+
next: [cliCmd(`delete ${slug} --yes`)]
|
|
5185
5281
|
});
|
|
5186
5282
|
} else {
|
|
5187
5283
|
log.err(error.message);
|
|
@@ -5197,14 +5293,14 @@ async function deleteCommand(slug, opts = {}, deps = { api }) {
|
|
|
5197
5293
|
skill: { slug, version: info.versionSeq, exists: false },
|
|
5198
5294
|
result: "deleted",
|
|
5199
5295
|
message: `Deleted ${slug} from the Library.`,
|
|
5200
|
-
next: ["
|
|
5296
|
+
next: [cliCmd("status")]
|
|
5201
5297
|
});
|
|
5202
5298
|
return;
|
|
5203
5299
|
}
|
|
5204
5300
|
log.ok(`Deleted ${slug} from the Library.`);
|
|
5205
5301
|
log.blank();
|
|
5206
5302
|
log.info("Local copies in your agents still have the skill. Floom will not delete them automatically.");
|
|
5207
|
-
printNext([{ command: "
|
|
5303
|
+
printNext([{ command: cliCmd("status"), description: "see which local copies are no longer in the Library" }]);
|
|
5208
5304
|
}
|
|
5209
5305
|
|
|
5210
5306
|
// src/commands/list.ts
|
|
@@ -5218,11 +5314,11 @@ async function listCommand(opts = {}) {
|
|
|
5218
5314
|
workspace: { name: "Library", signedIn: false },
|
|
5219
5315
|
error: { code: "AUTH_REQUIRED", message: "Not signed in." },
|
|
5220
5316
|
skills: [],
|
|
5221
|
-
next: ["
|
|
5317
|
+
next: [cliCmd("login")]
|
|
5222
5318
|
});
|
|
5223
5319
|
} else {
|
|
5224
5320
|
log.err("Not signed in.");
|
|
5225
|
-
printNext([{ command: "
|
|
5321
|
+
printNext([{ command: cliCmd("login") }]);
|
|
5226
5322
|
}
|
|
5227
5323
|
process.exitCode = 1;
|
|
5228
5324
|
return;
|
|
@@ -5237,7 +5333,7 @@ async function listCommand(opts = {}) {
|
|
|
5237
5333
|
workspace: { name: "Library", signedIn: true },
|
|
5238
5334
|
error: { code: fe?.code ?? "INTERNAL_ERROR", message: e.message },
|
|
5239
5335
|
skills: [],
|
|
5240
|
-
next: ["
|
|
5336
|
+
next: [cliCmd("login")]
|
|
5241
5337
|
});
|
|
5242
5338
|
process.exitCode = 1;
|
|
5243
5339
|
return;
|
|
@@ -5271,7 +5367,7 @@ async function listCommand(opts = {}) {
|
|
|
5271
5367
|
}))
|
|
5272
5368
|
};
|
|
5273
5369
|
}),
|
|
5274
|
-
next: result.total === 0 ? ["
|
|
5370
|
+
next: result.total === 0 ? [cliCmd("push")] : [cliCmd("pull"), cliCmd("status")]
|
|
5275
5371
|
});
|
|
5276
5372
|
return;
|
|
5277
5373
|
}
|
|
@@ -5286,7 +5382,7 @@ async function listCommand(opts = {}) {
|
|
|
5286
5382
|
log.info(` ${tildePath(skill.path)}`);
|
|
5287
5383
|
}
|
|
5288
5384
|
}
|
|
5289
|
-
printNext([{ command: "
|
|
5385
|
+
printNext([{ command: cliCmd("push"), description: "publish your first skills to the Library" }]);
|
|
5290
5386
|
return;
|
|
5291
5387
|
}
|
|
5292
5388
|
log.heading(`Library \xB7 ${result.total} skill${result.total === 1 ? "" : "s"}`);
|
|
@@ -5304,8 +5400,8 @@ async function listCommand(opts = {}) {
|
|
|
5304
5400
|
log.info(` ... and ${result.total - rows.length} more`);
|
|
5305
5401
|
}
|
|
5306
5402
|
printNext([
|
|
5307
|
-
{ command: "
|
|
5308
|
-
{ command: "
|
|
5403
|
+
{ command: cliCmd("pull"), description: "install missing skills into local agents" },
|
|
5404
|
+
{ command: cliCmd("status"), description: "compare every local copy in detail" }
|
|
5309
5405
|
]);
|
|
5310
5406
|
}
|
|
5311
5407
|
|
|
@@ -5370,10 +5466,10 @@ async function pullCommand(skillArg, rawOpts = {}) {
|
|
|
5370
5466
|
const planMode = isPlanMode(flags);
|
|
5371
5467
|
if (!await readAuth()) {
|
|
5372
5468
|
if (json) {
|
|
5373
|
-
emitJson({ workspace: { name: "Library", signedIn: false }, mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], next: ["
|
|
5469
|
+
emitJson({ workspace: { name: "Library", signedIn: false }, mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], next: [cliCmd("login")] });
|
|
5374
5470
|
} else {
|
|
5375
5471
|
log.info("Not signed in to Floom.");
|
|
5376
|
-
printNext([{ command: "
|
|
5472
|
+
printNext([{ command: cliCmd("login") }]);
|
|
5377
5473
|
}
|
|
5378
5474
|
process.exitCode = 2;
|
|
5379
5475
|
return;
|
|
@@ -5381,14 +5477,14 @@ async function pullCommand(skillArg, rawOpts = {}) {
|
|
|
5381
5477
|
const detected = await detectAgents();
|
|
5382
5478
|
if (detected.length === 0) {
|
|
5383
5479
|
if (json) {
|
|
5384
|
-
emitJson({ workspace: { name: "Library", signedIn: true }, mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], next: ["
|
|
5480
|
+
emitJson({ workspace: { name: "Library", signedIn: true }, mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], next: [cliCmd("pull --agent claude")] });
|
|
5385
5481
|
} else {
|
|
5386
5482
|
log.info("No AI agents found on this machine.");
|
|
5387
5483
|
log.blank();
|
|
5388
5484
|
log.info("Floom looks for Claude, Codex, Cursor, Gemini, or OpenCode.");
|
|
5389
|
-
log.info(
|
|
5485
|
+
log.info(`Make sure one of these is installed, then run ${cliCmd("pull")} again.`);
|
|
5390
5486
|
log.blank();
|
|
5391
|
-
log.info(
|
|
5487
|
+
log.info(`Or use: ${cliCmd("pull --agent claude")}`);
|
|
5392
5488
|
log.blank();
|
|
5393
5489
|
log.info("More help: https://floom.dev/docs#agents");
|
|
5394
5490
|
}
|
|
@@ -5400,19 +5496,19 @@ async function pullCommand(skillArg, rawOpts = {}) {
|
|
|
5400
5496
|
if (skillArg) {
|
|
5401
5497
|
if (flags.allScopes) {
|
|
5402
5498
|
if (json) {
|
|
5403
|
-
emitJson({ workspace: { name: "Library", signedIn: true }, error: "
|
|
5499
|
+
emitJson({ workspace: { name: "Library", signedIn: true }, error: `${cliCmd("pull")} <skill> targets one copy \u2014 pass --global or --project, not --all-scopes.`, next: [cliCmd(`pull ${skillArg} --agent claude --global`)] });
|
|
5404
5500
|
} else {
|
|
5405
|
-
log.err("
|
|
5501
|
+
log.err(`${cliCmd("pull")} <skill> targets one copy \u2014 pass --global or --project, not --all-scopes.`);
|
|
5406
5502
|
}
|
|
5407
5503
|
process.exitCode = 2;
|
|
5408
5504
|
return;
|
|
5409
5505
|
}
|
|
5410
5506
|
if (flags.agents.length !== 1) {
|
|
5411
5507
|
if (json) {
|
|
5412
|
-
emitJson({ workspace: { name: "Library", signedIn: true }, error: "
|
|
5508
|
+
emitJson({ workspace: { name: "Library", signedIn: true }, error: `${cliCmd("pull")} <skill> needs exactly one --agent`, next: [cliCmd(`pull ${skillArg} --agent claude`)] });
|
|
5413
5509
|
} else {
|
|
5414
|
-
log.err(
|
|
5415
|
-
log.command(`
|
|
5510
|
+
log.err(`${cliCmd("pull")} ${skillArg} needs exactly one --agent <name>.`);
|
|
5511
|
+
log.command(cliCmd(`pull ${skillArg} --agent claude`));
|
|
5416
5512
|
}
|
|
5417
5513
|
process.exitCode = 2;
|
|
5418
5514
|
return;
|
|
@@ -5421,7 +5517,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
|
|
|
5421
5517
|
const hasProjectSkillsDir = await projectSkillsDirExists(process.cwd());
|
|
5422
5518
|
if (flags.scope === "project" && !hasProjectSkillsDir) {
|
|
5423
5519
|
if (json) {
|
|
5424
|
-
emitJson({ workspace: { name: "Library", signedIn: true }, error: "No project agent skill directory found. Pass --global or run from a project with agent skills.", next: [`
|
|
5520
|
+
emitJson({ workspace: { name: "Library", signedIn: true }, error: "No project agent skill directory found. Pass --global or run from a project with agent skills.", next: [cliCmd(`pull ${skillArg} --agent ${agent} --global`)] });
|
|
5425
5521
|
} else {
|
|
5426
5522
|
log.err("No project agent skill directory found. Pass --global or run from a project with agent skills.");
|
|
5427
5523
|
}
|
|
@@ -5430,7 +5526,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
|
|
|
5430
5526
|
}
|
|
5431
5527
|
if (flags.scope === null && hasProjectSkillsDir && !isInteractive()) {
|
|
5432
5528
|
if (json) {
|
|
5433
|
-
emitJson({ workspace: { name: "Library", signedIn: true }, error: "This project has its own agent skill copies. Pass --global or --project.", next: [`
|
|
5529
|
+
emitJson({ workspace: { name: "Library", signedIn: true }, error: "This project has its own agent skill copies. Pass --global or --project.", next: [cliCmd(`pull ${skillArg} --agent ${agent} --global`), cliCmd(`pull ${skillArg} --agent ${agent} --project`)] });
|
|
5434
5530
|
} else {
|
|
5435
5531
|
log.err("This project has its own agent skill copies. Pass --global or --project.");
|
|
5436
5532
|
}
|
|
@@ -5439,7 +5535,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
|
|
|
5439
5535
|
}
|
|
5440
5536
|
const resolvedScope = flags.scope === "project" ? "project" : "global";
|
|
5441
5537
|
const dir = resolve5(resolvedScope === "project" ? projectSkillsDir(agent) : globalSkillsDir(agent));
|
|
5442
|
-
const scopedPullCommand = `
|
|
5538
|
+
const scopedPullCommand = cliCmd(`pull ${skillArg} --agent ${agent}${resolvedScope === "project" ? " --project" : ""}`);
|
|
5443
5539
|
if (flags.dryRun || json && !flags.yes) {
|
|
5444
5540
|
if (json) {
|
|
5445
5541
|
emitJson({ workspace: { name: "Library", signedIn: true }, mode: "plan", applied: false, wouldMutate: true, hasSkipped: false, agents: [{ name: agent, scope: resolvedScope, skillsDir: dir, actions: [{ slug: skillArg, scope: resolvedScope, action: "update", fromVersion: null, toVersion: null, path: join9(dir, skillArg), backupPath: null }] }], next: [`${scopedPullCommand} --yes`] });
|
|
@@ -5455,10 +5551,10 @@ async function pullCommand(skillArg, rawOpts = {}) {
|
|
|
5455
5551
|
cleanup.trackDir(join9(dir, ".floom", "tmp"));
|
|
5456
5552
|
const result = await pullOneSkill(agent, skillArg, { installDir: dir });
|
|
5457
5553
|
if (json) {
|
|
5458
|
-
emitJson({ workspace: { name: "Library", signedIn: true }, mode: "apply", applied: true, hasFailures: false, agents: [{ name: agent, scope: resolvedScope, skillsDir: dir, results: [{ slug: result.slug, scope: resolvedScope, action: "update", result: "updated", fromVersion: null, toVersion: result.versionSeq, path: join9(dir, result.slug), backupPath: null, message: `updated to v${result.versionSeq}` }] }], next: [`
|
|
5554
|
+
emitJson({ workspace: { name: "Library", signedIn: true }, mode: "apply", applied: true, hasFailures: false, agents: [{ name: agent, scope: resolvedScope, skillsDir: dir, results: [{ slug: result.slug, scope: resolvedScope, action: "update", result: "updated", fromVersion: null, toVersion: result.versionSeq, path: join9(dir, result.slug), backupPath: null, message: `updated to v${result.versionSeq}` }] }], next: [cliCmd(`status --agent ${agent}`)] });
|
|
5459
5555
|
} else {
|
|
5460
5556
|
log.ok(`Backed up your local copy and updated ${result.slug} to Library v${result.versionSeq}`);
|
|
5461
|
-
printNext([{ command: `
|
|
5557
|
+
printNext([{ command: cliCmd(`status --agent ${agent}`) }]);
|
|
5462
5558
|
}
|
|
5463
5559
|
} catch (error) {
|
|
5464
5560
|
if (json) {
|
|
@@ -5472,16 +5568,16 @@ async function pullCommand(skillArg, rawOpts = {}) {
|
|
|
5472
5568
|
}
|
|
5473
5569
|
if (!planMode && flags.agents.length === 0 && !wantAll && !isInteractive()) {
|
|
5474
5570
|
if (json) {
|
|
5475
|
-
emitJson({ workspace: { name: "Library", signedIn: true }, error: "
|
|
5571
|
+
emitJson({ workspace: { name: "Library", signedIn: true }, error: `${cliCmd("pull")} needs to know which agents to update`, next: [cliCmd("pull --all-agents --yes")] });
|
|
5476
5572
|
} else {
|
|
5477
|
-
log.err("
|
|
5573
|
+
log.err(`${cliCmd("pull")} needs to know which agents to update.`);
|
|
5478
5574
|
log.blank();
|
|
5479
5575
|
log.info("Pass --agent <name>, repeat --agent, or use --all-agents.");
|
|
5480
5576
|
log.blank();
|
|
5481
5577
|
log.info("Examples:");
|
|
5482
|
-
log.command("
|
|
5483
|
-
log.command("
|
|
5484
|
-
log.command("
|
|
5578
|
+
log.command(cliCmd("pull --agent claude --yes"));
|
|
5579
|
+
log.command(cliCmd("pull --agent claude,codex --yes"));
|
|
5580
|
+
log.command(cliCmd("pull --all-agents --yes"));
|
|
5485
5581
|
}
|
|
5486
5582
|
process.exitCode = 2;
|
|
5487
5583
|
return;
|
|
@@ -5526,7 +5622,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
|
|
|
5526
5622
|
backupPath: null
|
|
5527
5623
|
}))
|
|
5528
5624
|
})),
|
|
5529
|
-
next: ["
|
|
5625
|
+
next: [cliCmd("pull --all-agents --yes")]
|
|
5530
5626
|
});
|
|
5531
5627
|
process.exitCode = (wouldMutate || hasSkipped) && !flags.exitZero ? 1 : 0;
|
|
5532
5628
|
return;
|
|
@@ -5564,7 +5660,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
|
|
|
5564
5660
|
}
|
|
5565
5661
|
log.blank();
|
|
5566
5662
|
const firstConflict = conflicted[0];
|
|
5567
|
-
printNext(firstConflict ? [{ command: `
|
|
5663
|
+
printNext(firstConflict ? [{ command: cliCmd(`diff ${firstConflict.slug}`), description: "review the conflict" }] : [{ command: cliCmd("status") }]);
|
|
5568
5664
|
process.exitCode = flags.exitZero ? 0 : 1;
|
|
5569
5665
|
return;
|
|
5570
5666
|
}
|
|
@@ -5573,7 +5669,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
|
|
|
5573
5669
|
for (const p of plans) {
|
|
5574
5670
|
log.row([AGENT_LABELS[p.agent], "up to date"], [10]);
|
|
5575
5671
|
}
|
|
5576
|
-
printNext([{ command: "
|
|
5672
|
+
printNext([{ command: cliCmd("status") }]);
|
|
5577
5673
|
return;
|
|
5578
5674
|
}
|
|
5579
5675
|
log.heading(`Update agent copies from ${workspaceName}`);
|
|
@@ -5590,7 +5686,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
|
|
|
5590
5686
|
let toUpdate = actionable;
|
|
5591
5687
|
if (!flags.yes) {
|
|
5592
5688
|
if (!isInteractive()) {
|
|
5593
|
-
log.err("
|
|
5689
|
+
log.err(`${cliCmd("pull")} needs a choice but is not running in an interactive terminal.`);
|
|
5594
5690
|
log.info("Re-run with --agent <name> --yes, or --all-agents --yes.");
|
|
5595
5691
|
process.exitCode = 2;
|
|
5596
5692
|
return;
|
|
@@ -5650,7 +5746,7 @@ async function pullCommand(skillArg, rawOpts = {}) {
|
|
|
5650
5746
|
message: a.message
|
|
5651
5747
|
}]
|
|
5652
5748
|
})),
|
|
5653
|
-
next: ["
|
|
5749
|
+
next: [cliCmd("status")]
|
|
5654
5750
|
});
|
|
5655
5751
|
if (failed || hasSkipped) process.exitCode = flags.exitZero ? 0 : 1;
|
|
5656
5752
|
return;
|
|
@@ -5663,8 +5759,8 @@ async function pullCommand(skillArg, rawOpts = {}) {
|
|
|
5663
5759
|
}
|
|
5664
5760
|
}
|
|
5665
5761
|
log.blank();
|
|
5666
|
-
log.info(
|
|
5667
|
-
printNext([{ command: "
|
|
5762
|
+
log.info(`Backups of any replaced skills are in each agent's .floom/backups/ folder (run ${cliCmd("restore --list")} to see them).`);
|
|
5763
|
+
printNext([{ command: cliCmd("status") }]);
|
|
5668
5764
|
if (failed || hasSkipped) process.exitCode = flags.exitZero ? 0 : 1;
|
|
5669
5765
|
void skillArg;
|
|
5670
5766
|
void INSTALL_TARGETS;
|
|
@@ -5674,6 +5770,9 @@ async function pullCommand(skillArg, rawOpts = {}) {
|
|
|
5674
5770
|
}
|
|
5675
5771
|
}
|
|
5676
5772
|
|
|
5773
|
+
// src/commands/sync.ts
|
|
5774
|
+
import chalk5 from "chalk";
|
|
5775
|
+
|
|
5677
5776
|
// src/commands/sync-runner.ts
|
|
5678
5777
|
init_src();
|
|
5679
5778
|
import { cp as cp2, mkdtemp, readdir as readdir4, rm as rm2, stat as stat6 } from "node:fs/promises";
|
|
@@ -5714,8 +5813,8 @@ function printPreview(plan) {
|
|
|
5714
5813
|
log.err(`${count} ${noun} both locally and on the server: Floom won't guess which wins.`);
|
|
5715
5814
|
for (const skill of plan.conflicts) {
|
|
5716
5815
|
log.err(` - ${skill.slug} (${skill.version})`);
|
|
5717
|
-
log.err(` Keep the server version:
|
|
5718
|
-
log.err(` Keep your local version:
|
|
5816
|
+
log.err(` Keep the server version: ${cliCmd(`pull --target ${plan.target}`)}`);
|
|
5817
|
+
log.err(` Keep your local version: ${cliCmd(`push <${skill.slug}-dir>`)}`);
|
|
5719
5818
|
}
|
|
5720
5819
|
log.err("Your local copy is always backed up to .floom/backups/ first.");
|
|
5721
5820
|
log.err("More: https://floom.dev/docs#conflicts");
|
|
@@ -5841,11 +5940,13 @@ async function runSyncForTarget(options = {}, deps = {}) {
|
|
|
5841
5940
|
log.err(` snapshot at: ${failure.snapshotDir}`);
|
|
5842
5941
|
}
|
|
5843
5942
|
log.err("");
|
|
5844
|
-
log.err(`Re-run
|
|
5943
|
+
log.err(`Re-run \`${cliCmd(`sync --target ${plan.target}`)}\` after the network recovers.`);
|
|
5845
5944
|
process.exitCode = 1;
|
|
5846
5945
|
return { ok: false, pushFailures, hasConflicts: false };
|
|
5847
5946
|
}
|
|
5848
|
-
|
|
5947
|
+
if (!options.quietSuccess) {
|
|
5948
|
+
log.ok(`Sync complete. Pulled ${plan.pull.length} skills. Pushed ${pushed}/${plan.push.length} skills.`);
|
|
5949
|
+
}
|
|
5849
5950
|
return { ok: true, pushFailures: [], hasConflicts: false };
|
|
5850
5951
|
}
|
|
5851
5952
|
|
|
@@ -5853,6 +5954,13 @@ async function runSyncForTarget(options = {}, deps = {}) {
|
|
|
5853
5954
|
init_runtime();
|
|
5854
5955
|
init_prompt();
|
|
5855
5956
|
import { resolve as resolve6 } from "node:path";
|
|
5957
|
+
async function runTestSyncDelay(cleanup) {
|
|
5958
|
+
const delayMs = Number.parseInt(process.env.FLOOM_TEST_SYNC_DELAY_MS ?? "", 10);
|
|
5959
|
+
if (!Number.isFinite(delayMs) || delayMs <= 0) return;
|
|
5960
|
+
const trackDir = process.env.FLOOM_TEST_SYNC_TRACK_DIR;
|
|
5961
|
+
if (trackDir) cleanup.trackDir(trackDir);
|
|
5962
|
+
await new Promise((resolve14) => setTimeout(resolve14, delayMs));
|
|
5963
|
+
}
|
|
5856
5964
|
function appliedFromResult(plan, result) {
|
|
5857
5965
|
if (result.ok) return { plan, ok: true, message: "synced" };
|
|
5858
5966
|
const message = result.pushFailures.length > 0 ? `push failed for ${result.pushFailures.map((f) => f.slug).join(", ")}` : result.hasConflicts ? "changed in two places" : "sync did not complete";
|
|
@@ -5874,9 +5982,12 @@ function buildApplyJson(workspaceName, applied, opts) {
|
|
|
5874
5982
|
...a.plan.conflicts.map((slug) => ({ slug, scope: a.plan.scope, direction: "pull", result: "skipped", fromVersion: null, toVersion: null, path: a.plan.skillsDir, backupPath: null, message: "changed in two places" }))
|
|
5875
5983
|
]
|
|
5876
5984
|
})),
|
|
5877
|
-
next: ["
|
|
5985
|
+
next: [cliCmd("status")]
|
|
5878
5986
|
};
|
|
5879
5987
|
}
|
|
5988
|
+
function formatAgentLabel(plan) {
|
|
5989
|
+
return `${AGENT_LABELS[plan.agent]}${plan.scope === "project" ? " (project)" : ""}`;
|
|
5990
|
+
}
|
|
5880
5991
|
async function planForAgent(agent, scope, skillsDir, statusFn = statusLibrary) {
|
|
5881
5992
|
try {
|
|
5882
5993
|
const status = await statusFn(agent, { installDir: skillsDir });
|
|
@@ -5900,6 +6011,7 @@ async function syncCommand(rawOpts = {}, deps = {}) {
|
|
|
5900
6011
|
const readAuthFn = deps.readAuth ?? readAuth;
|
|
5901
6012
|
const cleanup = installCancellationHandler();
|
|
5902
6013
|
try {
|
|
6014
|
+
await runTestSyncDelay(cleanup);
|
|
5903
6015
|
let flags;
|
|
5904
6016
|
try {
|
|
5905
6017
|
flags = parseCommonFlags(rawOpts);
|
|
@@ -5915,10 +6027,10 @@ async function syncCommand(rawOpts = {}, deps = {}) {
|
|
|
5915
6027
|
const planMode = isPlanMode(flags);
|
|
5916
6028
|
if (!await readAuthFn()) {
|
|
5917
6029
|
if (json) {
|
|
5918
|
-
emitJson({ workspace: { name: "Library", signedIn: false }, mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], next: ["
|
|
6030
|
+
emitJson({ workspace: { name: "Library", signedIn: false }, mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], next: [cliCmd("login")] });
|
|
5919
6031
|
} else {
|
|
5920
6032
|
log.info("Not signed in to Floom.");
|
|
5921
|
-
printNext([{ command: "
|
|
6033
|
+
printNext([{ command: cliCmd("login") }]);
|
|
5922
6034
|
}
|
|
5923
6035
|
process.exitCode = 2;
|
|
5924
6036
|
return;
|
|
@@ -5926,7 +6038,7 @@ async function syncCommand(rawOpts = {}, deps = {}) {
|
|
|
5926
6038
|
const detected = await detectAgentsFn();
|
|
5927
6039
|
if (detected.length === 0) {
|
|
5928
6040
|
if (json) {
|
|
5929
|
-
emitJson({ workspace: { name: "Library", signedIn: true }, mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], error: "no agents detected \u2014 install Claude, Codex, Cursor, Gemini, or OpenCode", next: ["
|
|
6041
|
+
emitJson({ workspace: { name: "Library", signedIn: true }, mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], error: "no agents detected \u2014 install Claude, Codex, Cursor, Gemini, or OpenCode", next: [cliCmd("sync --agent claude")] });
|
|
5930
6042
|
} else {
|
|
5931
6043
|
log.err("No agents detected \u2014 install Claude, Codex, Cursor, Gemini, or OpenCode, then re-run.");
|
|
5932
6044
|
log.info("Floom syncs into one of: Claude, Codex, Cursor, Gemini, OpenCode.");
|
|
@@ -5939,14 +6051,14 @@ async function syncCommand(rawOpts = {}, deps = {}) {
|
|
|
5939
6051
|
const selectedAgents = flags.agents.length > 0 ? flags.agents : detected;
|
|
5940
6052
|
if (!planMode && !flags.yes && !isInteractive()) {
|
|
5941
6053
|
if (json) {
|
|
5942
|
-
emitJson({ workspace: { name: "Library", signedIn: true }, error: "
|
|
6054
|
+
emitJson({ workspace: { name: "Library", signedIn: true }, error: `${cliCmd("sync")} needs --yes to apply changes in a non-interactive shell`, next: [cliCmd("sync --yes")] });
|
|
5943
6055
|
} else {
|
|
5944
|
-
log.err("
|
|
6056
|
+
log.err(`${cliCmd("sync")} needs --yes to apply changes in a non-interactive shell.`);
|
|
5945
6057
|
log.blank();
|
|
5946
6058
|
log.info("Examples:");
|
|
5947
|
-
log.command("
|
|
5948
|
-
log.command("
|
|
5949
|
-
log.command("
|
|
6059
|
+
log.command(cliCmd("sync --yes") + " # sync every detected agent");
|
|
6060
|
+
log.command(cliCmd("sync --agent claude --yes"));
|
|
6061
|
+
log.command(cliCmd("sync --agent claude,codex --yes"));
|
|
5950
6062
|
}
|
|
5951
6063
|
process.exitCode = 2;
|
|
5952
6064
|
return;
|
|
@@ -5990,12 +6102,12 @@ async function syncCommand(rawOpts = {}, deps = {}) {
|
|
|
5990
6102
|
reason: "changed_in_two_places",
|
|
5991
6103
|
libraryVersion: 0,
|
|
5992
6104
|
localPath: p.skillsDir,
|
|
5993
|
-
diffCommand: `
|
|
5994
|
-
pullCommand: `
|
|
5995
|
-
pushCommand:
|
|
6105
|
+
diffCommand: cliCmd(`diff ${slug}`),
|
|
6106
|
+
pullCommand: cliCmd(`pull ${slug} --agent ${p.agent}${p.scope === "project" ? " --project" : ""}`),
|
|
6107
|
+
pushCommand: cliCmd("push")
|
|
5996
6108
|
}))
|
|
5997
6109
|
})),
|
|
5998
|
-
next: ["
|
|
6110
|
+
next: [cliCmd("sync --all-agents --yes")]
|
|
5999
6111
|
});
|
|
6000
6112
|
process.exitCode = (wouldMutate || hasSkipped) && !flags.exitZero ? 1 : 0;
|
|
6001
6113
|
return;
|
|
@@ -6029,7 +6141,7 @@ async function syncCommand(rawOpts = {}, deps = {}) {
|
|
|
6029
6141
|
log.info("Everything is in sync.");
|
|
6030
6142
|
log.blank();
|
|
6031
6143
|
for (const p of plans) log.row([AGENT_LABELS[p.agent], "up to date"], [10]);
|
|
6032
|
-
printNext([{ command: "
|
|
6144
|
+
printNext([{ command: cliCmd("status") }]);
|
|
6033
6145
|
return;
|
|
6034
6146
|
}
|
|
6035
6147
|
if (defaultedToAll && detected.length > 1) {
|
|
@@ -6048,7 +6160,7 @@ async function syncCommand(rawOpts = {}, deps = {}) {
|
|
|
6048
6160
|
let toSync = actionable;
|
|
6049
6161
|
if (!flags.yes) {
|
|
6050
6162
|
if (!isInteractive()) {
|
|
6051
|
-
log.err("
|
|
6163
|
+
log.err(`${cliCmd("sync")} needs a choice but is not running in an interactive terminal.`);
|
|
6052
6164
|
log.info("Re-run with --agent <name> --yes, or --all-agents --yes.");
|
|
6053
6165
|
process.exitCode = 2;
|
|
6054
6166
|
return;
|
|
@@ -6073,7 +6185,7 @@ async function syncCommand(rawOpts = {}, deps = {}) {
|
|
|
6073
6185
|
const applied = [];
|
|
6074
6186
|
for (const p of toSync) {
|
|
6075
6187
|
try {
|
|
6076
|
-
const result = await runSyncFn({ target: p.agent, yes: true, installDir: p.skillsDir });
|
|
6188
|
+
const result = await runSyncFn({ target: p.agent, yes: true, installDir: p.skillsDir, quietSuccess: true });
|
|
6077
6189
|
const entry = appliedFromResult(p, result);
|
|
6078
6190
|
if (!entry.ok) failed = true;
|
|
6079
6191
|
applied.push(entry);
|
|
@@ -6088,6 +6200,32 @@ async function syncCommand(rawOpts = {}, deps = {}) {
|
|
|
6088
6200
|
if (failed || hasSkipped) process.exitCode = 1;
|
|
6089
6201
|
return;
|
|
6090
6202
|
}
|
|
6203
|
+
if (failed) {
|
|
6204
|
+
const failedCount = applied.filter((a) => !a.ok).length;
|
|
6205
|
+
const succeededCount = applied.filter((a) => a.ok).length;
|
|
6206
|
+
log.blank();
|
|
6207
|
+
log.info(`Sync completed with ${failedCount} target(s) failing (${succeededCount} succeeded).`);
|
|
6208
|
+
for (const entry of applied) {
|
|
6209
|
+
const marker = entry.ok ? chalk5.green("\u2713") : chalk5.red("\u2717");
|
|
6210
|
+
log.info(` ${marker} ${formatAgentLabel(entry.plan)} ${entry.message}`);
|
|
6211
|
+
}
|
|
6212
|
+
log.blank();
|
|
6213
|
+
printNext([{ command: cliCmd("status") }]);
|
|
6214
|
+
if (hasSkipped) {
|
|
6215
|
+
for (const p of plans) {
|
|
6216
|
+
for (const slug of p.conflicts) {
|
|
6217
|
+
log.warn(`${AGENT_LABELS[p.agent]} skipped ${slug}, changed in two places`);
|
|
6218
|
+
}
|
|
6219
|
+
}
|
|
6220
|
+
}
|
|
6221
|
+
process.exitCode = 1;
|
|
6222
|
+
return;
|
|
6223
|
+
}
|
|
6224
|
+
if (!hasSkipped) {
|
|
6225
|
+
const pulled = applied.reduce((n, a) => n + a.plan.pull.length, 0);
|
|
6226
|
+
const pushed = applied.reduce((n, a) => n + a.plan.push.length, 0);
|
|
6227
|
+
log.ok(`Sync complete. Pulled ${pulled} skill${pulled === 1 ? "" : "s"}. Pushed ${pushed}/${pushed} skill${pushed === 1 ? "" : "s"}.`);
|
|
6228
|
+
}
|
|
6091
6229
|
for (const p of plans) {
|
|
6092
6230
|
for (const slug of p.conflicts) {
|
|
6093
6231
|
log.warn(`${AGENT_LABELS[p.agent]} skipped ${slug}, changed in two places`);
|
|
@@ -6096,15 +6234,15 @@ async function syncCommand(rawOpts = {}, deps = {}) {
|
|
|
6096
6234
|
if (hasSkipped) {
|
|
6097
6235
|
const conflictSlug = plans.flatMap((p) => p.conflicts)[0];
|
|
6098
6236
|
printNext([
|
|
6099
|
-
...conflictSlug ? [{ command: `
|
|
6100
|
-
{ command: "
|
|
6237
|
+
...conflictSlug ? [{ command: cliCmd(`diff ${conflictSlug}`), description: "show what changed before choosing" }] : [],
|
|
6238
|
+
{ command: cliCmd("status") }
|
|
6101
6239
|
]);
|
|
6102
6240
|
} else {
|
|
6103
6241
|
log.blank();
|
|
6104
|
-
log.info(
|
|
6105
|
-
printNext([{ command: "
|
|
6242
|
+
log.info(`Backups of any replaced skills are in each agent's .floom/backups/ folder (run ${cliCmd("restore --list")} to see them).`);
|
|
6243
|
+
printNext([{ command: cliCmd("status") }]);
|
|
6106
6244
|
}
|
|
6107
|
-
if (
|
|
6245
|
+
if (hasSkipped) process.exitCode = 1;
|
|
6108
6246
|
} finally {
|
|
6109
6247
|
cleanup.dispose();
|
|
6110
6248
|
}
|
|
@@ -6112,20 +6250,20 @@ async function syncCommand(rawOpts = {}, deps = {}) {
|
|
|
6112
6250
|
|
|
6113
6251
|
// src/commands/status.ts
|
|
6114
6252
|
import { resolve as resolve7 } from "node:path";
|
|
6115
|
-
import
|
|
6253
|
+
import chalk6 from "chalk";
|
|
6116
6254
|
init_runtime();
|
|
6117
6255
|
var AGENT_TIMEOUT_MS = 1e4;
|
|
6118
6256
|
var MAX_ATTENTION_ROWS = 15;
|
|
6119
6257
|
function toFloomState(state) {
|
|
6120
6258
|
switch (state) {
|
|
6121
6259
|
case "active":
|
|
6122
|
-
return { state: "up_to_date", label: "up to date", color:
|
|
6260
|
+
return { state: "up_to_date", label: "up to date", color: chalk6.green };
|
|
6123
6261
|
case "stale":
|
|
6124
|
-
return { state: "update_available", label: "update available", color:
|
|
6262
|
+
return { state: "update_available", label: "update available", color: chalk6.yellow };
|
|
6125
6263
|
case "dirty":
|
|
6126
|
-
return { state: "local_changes", label: "local changes", color:
|
|
6264
|
+
return { state: "local_changes", label: "local changes", color: chalk6.yellow };
|
|
6127
6265
|
case "conflict":
|
|
6128
|
-
return { state: "changed_in_two_places", label: "changed in two places", color:
|
|
6266
|
+
return { state: "changed_in_two_places", label: "changed in two places", color: chalk6.red };
|
|
6129
6267
|
case "missing":
|
|
6130
6268
|
return { state: "not_installed", label: "not installed", color: (s) => s };
|
|
6131
6269
|
case "unsupported_target":
|
|
@@ -6181,15 +6319,15 @@ async function statusCommand(rawOpts = {}) {
|
|
|
6181
6319
|
const detected = await detectAgents();
|
|
6182
6320
|
if (detected.length === 0) {
|
|
6183
6321
|
if (json) {
|
|
6184
|
-
emitJson({ workspace: { name: "Library", signedIn: false, librarySkillCount: null }, complete: true, agents: [], skillsNeedingAttention: [], next: ["
|
|
6322
|
+
emitJson({ workspace: { name: "Library", signedIn: false, librarySkillCount: null }, complete: true, agents: [], skillsNeedingAttention: [], next: [cliCmd("install <link>")] });
|
|
6185
6323
|
} else {
|
|
6186
6324
|
log.info("No AI agents found on this machine.");
|
|
6187
6325
|
log.blank();
|
|
6188
6326
|
log.info("Floom looks for Claude, Codex, Cursor, Gemini, or OpenCode.");
|
|
6189
|
-
log.info(
|
|
6327
|
+
log.info(`Install one of those agents, then run ${cliCmd("status")} again.`);
|
|
6190
6328
|
log.blank();
|
|
6191
6329
|
log.info("If someone sent you a shared skill:");
|
|
6192
|
-
log.command("
|
|
6330
|
+
log.command(cliCmd("install <link>"));
|
|
6193
6331
|
log.blank();
|
|
6194
6332
|
log.info("More help: https://floom.dev/docs#agents");
|
|
6195
6333
|
}
|
|
@@ -6205,7 +6343,7 @@ async function statusCommand(rawOpts = {}) {
|
|
|
6205
6343
|
complete: false,
|
|
6206
6344
|
agents: detected.map((agent) => ({ name: agent, detected: true, state: null, error: null, summary: emptySummary(), copies: [] })),
|
|
6207
6345
|
skillsNeedingAttention: [],
|
|
6208
|
-
next: ["
|
|
6346
|
+
next: [cliCmd("login")]
|
|
6209
6347
|
});
|
|
6210
6348
|
process.exitCode = 1;
|
|
6211
6349
|
return;
|
|
@@ -6219,7 +6357,7 @@ async function statusCommand(rawOpts = {}) {
|
|
|
6219
6357
|
log.blank();
|
|
6220
6358
|
log.info("Floom Library");
|
|
6221
6359
|
log.info(" Not signed in \u2014 can't compare with your team Library.");
|
|
6222
|
-
printNext([{ command: "
|
|
6360
|
+
printNext([{ command: cliCmd("login"), description: "see and sync your Library" }]);
|
|
6223
6361
|
return;
|
|
6224
6362
|
}
|
|
6225
6363
|
const me = await fetchMe().catch(() => null);
|
|
@@ -6275,7 +6413,7 @@ async function statusCommand(rawOpts = {}) {
|
|
|
6275
6413
|
};
|
|
6276
6414
|
}),
|
|
6277
6415
|
skillsNeedingAttention: buildAttention(checks),
|
|
6278
|
-
next: ["
|
|
6416
|
+
next: [cliCmd("sync"), cliCmd("status --verbose")]
|
|
6279
6417
|
});
|
|
6280
6418
|
process.exitCode = complete ? 0 : 1;
|
|
6281
6419
|
return;
|
|
@@ -6291,8 +6429,8 @@ async function statusCommand(rawOpts = {}) {
|
|
|
6291
6429
|
}
|
|
6292
6430
|
}
|
|
6293
6431
|
printNext([
|
|
6294
|
-
{ command: "
|
|
6295
|
-
{ command: "
|
|
6432
|
+
{ command: cliCmd("push"), description: "publish local changes" },
|
|
6433
|
+
{ command: cliCmd("pull"), description: "install Library updates" }
|
|
6296
6434
|
]);
|
|
6297
6435
|
return;
|
|
6298
6436
|
}
|
|
@@ -6301,14 +6439,14 @@ async function statusCommand(rawOpts = {}) {
|
|
|
6301
6439
|
if (attention.length === 0 && timedOut.length === 0) {
|
|
6302
6440
|
log.blank();
|
|
6303
6441
|
log.info(`All ${skillCount} skill${skillCount === 1 ? "" : "s"} are up to date in ${selectedAgents.map((a) => AGENT_LABELS[a]).join(", ")}.`);
|
|
6304
|
-
printNext([{ command: "
|
|
6442
|
+
printNext([{ command: cliCmd("push"), description: "if you just created a new skill locally" }]);
|
|
6305
6443
|
return;
|
|
6306
6444
|
}
|
|
6307
6445
|
log.heading("Skills needing attention");
|
|
6308
6446
|
log.blank();
|
|
6309
6447
|
const shown = attention.slice(0, MAX_ATTENTION_ROWS);
|
|
6310
6448
|
for (const item of shown) {
|
|
6311
|
-
log.info(` ${
|
|
6449
|
+
log.info(` ${chalk6.bold(item.slug)}`);
|
|
6312
6450
|
for (const loc of item.locations) {
|
|
6313
6451
|
const c = toFloomLabel(loc.state);
|
|
6314
6452
|
log.info(` ${AGENT_LABELS[loc.agent].padEnd(10)} ${c.color(c.label.padEnd(22))}${tildePath(loc.path)}`);
|
|
@@ -6316,7 +6454,7 @@ async function statusCommand(rawOpts = {}) {
|
|
|
6316
6454
|
}
|
|
6317
6455
|
if (attention.length > shown.length) {
|
|
6318
6456
|
log.blank();
|
|
6319
|
-
log.info(`...and ${attention.length - shown.length} more \u2014 run
|
|
6457
|
+
log.info(`...and ${attention.length - shown.length} more \u2014 run ${cliCmd("status --verbose")}.`);
|
|
6320
6458
|
}
|
|
6321
6459
|
const upToDate = checks.flatMap((c) => c.skills).filter((s) => s.state === "up_to_date").length;
|
|
6322
6460
|
if (upToDate > 0) {
|
|
@@ -6327,15 +6465,15 @@ async function statusCommand(rawOpts = {}) {
|
|
|
6327
6465
|
if (timedOut.length > 0) {
|
|
6328
6466
|
log.blank();
|
|
6329
6467
|
log.info("Status summary");
|
|
6330
|
-
log.info(` ${timedOut.length} agent${timedOut.length === 1 ? "" : "s"} timed out \u2014 run
|
|
6468
|
+
log.info(` ${timedOut.length} agent${timedOut.length === 1 ? "" : "s"} timed out \u2014 run ${cliCmd(`status --agent ${timedOut[0].agent}`)} to retry.`);
|
|
6331
6469
|
}
|
|
6332
6470
|
const hasLocalUnpublished = attention.some((a) => a.locations.some((l) => l.state === "not_in_library_never_published" || l.state === "local_changes"));
|
|
6333
6471
|
const hasUpdates = attention.some((a) => a.locations.some((l) => l.state === "update_available"));
|
|
6334
6472
|
const next = [];
|
|
6335
|
-
if (hasLocalUnpublished && hasUpdates) next.push({ command: "
|
|
6336
|
-
if (hasLocalUnpublished) next.push({ command: "
|
|
6337
|
-
if (hasUpdates) next.push({ command: "
|
|
6338
|
-
next.push({ command: "
|
|
6473
|
+
if (hasLocalUnpublished && hasUpdates) next.push({ command: cliCmd("sync"), description: "review local changes and Library updates together" });
|
|
6474
|
+
if (hasLocalUnpublished) next.push({ command: cliCmd("push"), description: "publish local skills to the Library" });
|
|
6475
|
+
if (hasUpdates) next.push({ command: cliCmd("pull"), description: "install Library updates" });
|
|
6476
|
+
next.push({ command: cliCmd("status --verbose"), description: "see every skill" });
|
|
6339
6477
|
printNext(next);
|
|
6340
6478
|
if (timedOut.length > 0) process.exitCode = 1;
|
|
6341
6479
|
}
|
|
@@ -6385,13 +6523,13 @@ function summarize(skills) {
|
|
|
6385
6523
|
function toFloomLabel(state) {
|
|
6386
6524
|
switch (state) {
|
|
6387
6525
|
case "up_to_date":
|
|
6388
|
-
return { label: "up to date", color:
|
|
6526
|
+
return { label: "up to date", color: chalk6.green };
|
|
6389
6527
|
case "local_changes":
|
|
6390
|
-
return { label: "local changes", color:
|
|
6528
|
+
return { label: "local changes", color: chalk6.yellow };
|
|
6391
6529
|
case "update_available":
|
|
6392
|
-
return { label: "update available", color:
|
|
6530
|
+
return { label: "update available", color: chalk6.yellow };
|
|
6393
6531
|
case "changed_in_two_places":
|
|
6394
|
-
return { label: "changed in two places", color:
|
|
6532
|
+
return { label: "changed in two places", color: chalk6.red };
|
|
6395
6533
|
case "not_in_library_never_published":
|
|
6396
6534
|
return { label: "not in Library", color: (s) => s };
|
|
6397
6535
|
case "not_in_library_removed":
|
|
@@ -6430,7 +6568,7 @@ var MACHINE_FILE3 = join11(CONFIG_DIR3, "machine.json");
|
|
|
6430
6568
|
async function renameDeviceCommand(newLabel) {
|
|
6431
6569
|
const trimmed = (newLabel ?? "").trim().slice(0, 80);
|
|
6432
6570
|
if (!trimmed) {
|
|
6433
|
-
log.err(
|
|
6571
|
+
log.err(`Device name cannot be empty. Use: ${cliCmd('rename-device "Work Laptop"')}`);
|
|
6434
6572
|
process.exitCode = 2;
|
|
6435
6573
|
return;
|
|
6436
6574
|
}
|
|
@@ -6451,8 +6589,8 @@ async function renameDeviceCommand(newLabel) {
|
|
|
6451
6589
|
log.blank();
|
|
6452
6590
|
log.info("Server sync will happen after your next login.");
|
|
6453
6591
|
printNext([
|
|
6454
|
-
{ command: "
|
|
6455
|
-
{ command: "
|
|
6592
|
+
{ command: cliCmd("login"), description: "sync the device name to your workspace" },
|
|
6593
|
+
{ command: cliCmd("account"), description: "see account details including device name" }
|
|
6456
6594
|
]);
|
|
6457
6595
|
return;
|
|
6458
6596
|
}
|
|
@@ -6469,7 +6607,7 @@ async function renameDeviceCommand(newLabel) {
|
|
|
6469
6607
|
}
|
|
6470
6608
|
log.blank();
|
|
6471
6609
|
log.info("This name appears in your workspace when teammates look at which machine synced last.");
|
|
6472
|
-
printNext([{ command: "
|
|
6610
|
+
printNext([{ command: cliCmd("account"), description: "see account details including device name" }]);
|
|
6473
6611
|
}
|
|
6474
6612
|
|
|
6475
6613
|
// src/commands/install.ts
|
|
@@ -6646,7 +6784,7 @@ async function installCommand(input, rawOpts = {}) {
|
|
|
6646
6784
|
const detected = await detectAgents();
|
|
6647
6785
|
if (detected.length === 0) {
|
|
6648
6786
|
if (json) {
|
|
6649
|
-
emitJson({ share: shareInfo(shareData), mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], next: ["
|
|
6787
|
+
emitJson({ share: shareInfo(shareData), mode: "plan", applied: false, wouldMutate: false, hasSkipped: false, agents: [], next: [cliCmd("install <link> --agent claude")] });
|
|
6650
6788
|
} else {
|
|
6651
6789
|
log.heading("Shared skill");
|
|
6652
6790
|
log.info(` ${shareData.skill.title} ${shareData.skill.version.display}`);
|
|
@@ -6667,16 +6805,16 @@ async function installCommand(input, rawOpts = {}) {
|
|
|
6667
6805
|
if (!planMode && selectedAgents.length === 0) {
|
|
6668
6806
|
if (!isInteractive()) {
|
|
6669
6807
|
if (json) {
|
|
6670
|
-
emitJson({ share: shareInfo(shareData), error: "
|
|
6808
|
+
emitJson({ share: shareInfo(shareData), error: `${cliCmd("install")} needs to know where to install`, next: [cliCmd("install <link> --all-agents --yes")] });
|
|
6671
6809
|
} else {
|
|
6672
|
-
log.err("
|
|
6810
|
+
log.err(`${cliCmd("install")} needs to know where to install.`);
|
|
6673
6811
|
log.blank();
|
|
6674
6812
|
log.info("Pass --agent <name>, repeat --agent, or use --all-agents.");
|
|
6675
6813
|
log.blank();
|
|
6676
6814
|
log.info("Examples:");
|
|
6677
|
-
log.command("
|
|
6678
|
-
log.command("
|
|
6679
|
-
log.command("
|
|
6815
|
+
log.command(cliCmd("install <link> --agent claude --yes"));
|
|
6816
|
+
log.command(cliCmd("install <link> --agent claude,codex --yes"));
|
|
6817
|
+
log.command(cliCmd("install <link> --all-agents --yes"));
|
|
6680
6818
|
}
|
|
6681
6819
|
process.exitCode = 2;
|
|
6682
6820
|
return;
|
|
@@ -6745,7 +6883,7 @@ async function installCommand(input, rawOpts = {}) {
|
|
|
6745
6883
|
wouldMutate: planAgents.length > 0,
|
|
6746
6884
|
hasSkipped: false,
|
|
6747
6885
|
agents: planAgents,
|
|
6748
|
-
next: ["
|
|
6886
|
+
next: [cliCmd("install <link> --all-agents --yes")]
|
|
6749
6887
|
});
|
|
6750
6888
|
} else {
|
|
6751
6889
|
log.heading("Install plan");
|
|
@@ -6806,7 +6944,7 @@ async function installCommand(input, rawOpts = {}) {
|
|
|
6806
6944
|
backupPath: null,
|
|
6807
6945
|
message: r.message
|
|
6808
6946
|
})),
|
|
6809
|
-
next: ["
|
|
6947
|
+
next: [cliCmd("status")]
|
|
6810
6948
|
});
|
|
6811
6949
|
} else {
|
|
6812
6950
|
printNext([{ command: `Open your agent and ask it to use "${shareData.skill.title}".` }]);
|
|
@@ -6947,10 +7085,10 @@ async function newCommand(name, rawOpts = {}) {
|
|
|
6947
7085
|
const json = flags.json;
|
|
6948
7086
|
if (flags.allScopes) {
|
|
6949
7087
|
if (json) {
|
|
6950
|
-
emitJson({ skill: { slug: name, name: deriveTitle(name) }, mode: "plan", applied: false, wouldMutate: false, scope: null, path: null, action: "refused", reason: "needs_agent_choice", message: "
|
|
7088
|
+
emitJson({ skill: { slug: name, name: deriveTitle(name) }, mode: "plan", applied: false, wouldMutate: false, scope: null, path: null, action: "refused", reason: "needs_agent_choice", message: `${cliCmd("new")} creates one skill in one location; --all-scopes does not apply.`, next: [cliCmd(`new ${name} --agent claude`)] });
|
|
6951
7089
|
} else {
|
|
6952
|
-
log.err("
|
|
6953
|
-
printNext([{ command: `
|
|
7090
|
+
log.err(`${cliCmd("new")} creates one skill in one location; --all-scopes does not apply.`);
|
|
7091
|
+
printNext([{ command: cliCmd(`new ${name} --agent claude`) }]);
|
|
6954
7092
|
}
|
|
6955
7093
|
process.exitCode = 2;
|
|
6956
7094
|
return;
|
|
@@ -6963,7 +7101,7 @@ async function newCommand(name, rawOpts = {}) {
|
|
|
6963
7101
|
log.blank();
|
|
6964
7102
|
log.info("Skill names use lowercase letters, numbers, and hyphens only.");
|
|
6965
7103
|
log.info("Use:");
|
|
6966
|
-
log.command(`
|
|
7104
|
+
log.command(cliCmd(`new ${name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")}`));
|
|
6967
7105
|
}
|
|
6968
7106
|
process.exitCode = 2;
|
|
6969
7107
|
return;
|
|
@@ -6980,8 +7118,8 @@ async function newCommand(name, rawOpts = {}) {
|
|
|
6980
7118
|
log.info(`Floom will not create a nested skill inside ${tildePath(cwd)}.`);
|
|
6981
7119
|
printNext([
|
|
6982
7120
|
{ command: "cd ~/.claude/skills" },
|
|
6983
|
-
{ command: `
|
|
6984
|
-
{ command: `
|
|
7121
|
+
{ command: cliCmd(`new ${name}`) },
|
|
7122
|
+
{ command: cliCmd(`new ${name} --agent claude`) }
|
|
6985
7123
|
]);
|
|
6986
7124
|
}
|
|
6987
7125
|
process.exitCode = 2;
|
|
@@ -7014,7 +7152,7 @@ async function newCommand(name, rawOpts = {}) {
|
|
|
7014
7152
|
usesScannedDir = false;
|
|
7015
7153
|
} else if (!isInteractive() || json) {
|
|
7016
7154
|
if (json) {
|
|
7017
|
-
emitJson({ skill: { slug: name, name: title }, mode: "plan", applied: false, wouldMutate: true, scope: null, path: null, action: "refused", reason: "needs_agent_choice", message: "
|
|
7155
|
+
emitJson({ skill: { slug: name, name: title }, mode: "plan", applied: false, wouldMutate: true, scope: null, path: null, action: "refused", reason: "needs_agent_choice", message: `${cliCmd("new")} needs to know where to create the skill. Pass --agent <name>.`, next: [cliCmd(`new ${name} --agent ${detected[0]}`)] });
|
|
7018
7156
|
process.exitCode = 2;
|
|
7019
7157
|
return;
|
|
7020
7158
|
}
|
|
@@ -7023,8 +7161,8 @@ async function newCommand(name, rawOpts = {}) {
|
|
|
7023
7161
|
scope = "global";
|
|
7024
7162
|
usesScannedDir = true;
|
|
7025
7163
|
} else {
|
|
7026
|
-
log.err("
|
|
7027
|
-
printNext([{ command: `
|
|
7164
|
+
log.err(`${cliCmd("new")} needs to know where to create the skill.`);
|
|
7165
|
+
printNext([{ command: cliCmd(`new ${name} --agent ${detected[0]}`) }]);
|
|
7028
7166
|
process.exitCode = 2;
|
|
7029
7167
|
return;
|
|
7030
7168
|
}
|
|
@@ -7057,7 +7195,7 @@ async function newCommand(name, rawOpts = {}) {
|
|
|
7057
7195
|
}
|
|
7058
7196
|
if (flags.dryRun || json && !flags.yes) {
|
|
7059
7197
|
if (json) {
|
|
7060
|
-
emitJson({ skill: { slug: name, name: title }, mode: "plan", applied: false, wouldMutate: true, scope, path: skillFolder, action: "create", reason: null, message: `${name} would be created`, next: [usesScannedDir ? "
|
|
7198
|
+
emitJson({ skill: { slug: name, name: title }, mode: "plan", applied: false, wouldMutate: true, scope, path: skillFolder, action: "create", reason: null, message: `${name} would be created`, next: [usesScannedDir ? cliCmd("push") : cliCmd(`push ${skillFolder}`)] });
|
|
7061
7199
|
} else {
|
|
7062
7200
|
log.heading("Create plan");
|
|
7063
7201
|
log.blank();
|
|
@@ -7078,7 +7216,7 @@ async function newCommand(name, rawOpts = {}) {
|
|
|
7078
7216
|
await mkdir7(skillFolder, { recursive: true });
|
|
7079
7217
|
await writeFile6(skillMd, skillTemplate(title));
|
|
7080
7218
|
await writeFile6(join13(skillFolder, ".floomignore"), FLOOMIGNORE);
|
|
7081
|
-
const pushCmd = usesScannedDir ? "
|
|
7219
|
+
const pushCmd = usesScannedDir ? cliCmd("push") : cliCmd(`push ${tildePath(skillFolder)}`);
|
|
7082
7220
|
if (json) {
|
|
7083
7221
|
emitJson({ skill: { slug: name, name: title }, mode: "apply", applied: true, scope, path: skillFolder, result: "created", reason: null, message: `Created ${skillFolder}`, next: [pushCmd] });
|
|
7084
7222
|
return;
|
|
@@ -7089,7 +7227,7 @@ async function newCommand(name, rawOpts = {}) {
|
|
|
7089
7227
|
if (!usesScannedDir) {
|
|
7090
7228
|
log.blank();
|
|
7091
7229
|
log.info("No AI agents were detected, so Floom created the skill in this folder.");
|
|
7092
|
-
log.info(
|
|
7230
|
+
log.info(`Zero-arg \`${cliCmd("push")}\` will not find this folder until an agent is installed.`);
|
|
7093
7231
|
next.push({ command: `Edit ${tildePath(skillMd)}` });
|
|
7094
7232
|
} else {
|
|
7095
7233
|
next.push({ command: `Edit ${tildePath(skillMd)}` });
|
|
@@ -7150,20 +7288,20 @@ async function doctorCommand(opts = {}) {
|
|
|
7150
7288
|
const auth = await readAuth();
|
|
7151
7289
|
let authOk = false;
|
|
7152
7290
|
if (!auth) {
|
|
7153
|
-
checks.push({ name: "auth", status: "fail", message: "not signed in", fix: "
|
|
7291
|
+
checks.push({ name: "auth", status: "fail", message: "not signed in", fix: cliCmd("login") });
|
|
7154
7292
|
} else {
|
|
7155
7293
|
try {
|
|
7156
7294
|
const me = await fetchMe();
|
|
7157
7295
|
authOk = true;
|
|
7158
7296
|
checks.push({ name: "auth", status: "pass", message: `signed in as ${me.user.email}`, fix: null });
|
|
7159
7297
|
} catch {
|
|
7160
|
-
checks.push({ name: "auth", status: "fail", message: "session expired", fix: "
|
|
7298
|
+
checks.push({ name: "auth", status: "fail", message: "session expired", fix: cliCmd("login") });
|
|
7161
7299
|
}
|
|
7162
7300
|
}
|
|
7163
7301
|
if (authOk) {
|
|
7164
7302
|
checks.push({ name: "api", status: "pass", message: "https://floom.dev reachable", fix: null });
|
|
7165
7303
|
} else {
|
|
7166
|
-
checks.push({ name: "api", status: "warn", message: "skipped \u2014 sign in first", fix: "
|
|
7304
|
+
checks.push({ name: "api", status: "warn", message: "skipped \u2014 sign in first", fix: cliCmd("login") });
|
|
7167
7305
|
}
|
|
7168
7306
|
const agents = await detectAgents();
|
|
7169
7307
|
if (agents.length === 0) {
|
|
@@ -7186,7 +7324,7 @@ async function doctorCommand(opts = {}) {
|
|
|
7186
7324
|
}
|
|
7187
7325
|
}
|
|
7188
7326
|
if (manifestCorrupt && corruptAgent) {
|
|
7189
|
-
const fix = authOk ? `
|
|
7327
|
+
const fix = authOk ? cliCmd(`sync --dry-run --agent ${corruptAgent}`) : cliCmd("login");
|
|
7190
7328
|
checks.push({
|
|
7191
7329
|
name: "manifests",
|
|
7192
7330
|
status: "fail",
|
|
@@ -7221,7 +7359,7 @@ async function doctorCommand(opts = {}) {
|
|
|
7221
7359
|
name: "backups",
|
|
7222
7360
|
status: "warn",
|
|
7223
7361
|
message: `${formatBytes(backupBytes)} across detected agents`,
|
|
7224
|
-
fix: "
|
|
7362
|
+
fix: cliCmd("restore --list")
|
|
7225
7363
|
});
|
|
7226
7364
|
} else {
|
|
7227
7365
|
checks.push({ name: "backups", status: "pass", message: `${formatBytes(backupBytes)} across ${agents.length} agent${agents.length === 1 ? "" : "s"}`, fix: null });
|
|
@@ -7241,7 +7379,7 @@ async function doctorCommand(opts = {}) {
|
|
|
7241
7379
|
emitJson({
|
|
7242
7380
|
ok,
|
|
7243
7381
|
checks,
|
|
7244
|
-
next: failures.length > 0 ? failures.map((c) => c.fix).filter((f) => Boolean(f)) : ["
|
|
7382
|
+
next: failures.length > 0 ? failures.map((c) => c.fix).filter((f) => Boolean(f)) : [cliCmd("status")]
|
|
7245
7383
|
});
|
|
7246
7384
|
process.exitCode = ok ? 0 : 1;
|
|
7247
7385
|
return;
|
|
@@ -7271,13 +7409,13 @@ async function doctorCommand(opts = {}) {
|
|
|
7271
7409
|
if (authFail && manifestCorrupt) {
|
|
7272
7410
|
log.blank();
|
|
7273
7411
|
log.info("Fix in this order:");
|
|
7274
|
-
log.command("
|
|
7275
|
-
if (corruptAgent) log.command(`
|
|
7412
|
+
log.command(cliCmd("login"));
|
|
7413
|
+
if (corruptAgent) log.command(cliCmd(`sync --dry-run --agent ${corruptAgent}`));
|
|
7276
7414
|
} else {
|
|
7277
|
-
printNext(failures.map((c) => ({ command: c.fix ?? "
|
|
7415
|
+
printNext(failures.map((c) => ({ command: c.fix ?? cliCmd("status") })));
|
|
7278
7416
|
}
|
|
7279
7417
|
} else {
|
|
7280
|
-
printNext([{ command: "
|
|
7418
|
+
printNext([{ command: cliCmd("status") }]);
|
|
7281
7419
|
}
|
|
7282
7420
|
process.exitCode = ok ? 0 : 1;
|
|
7283
7421
|
}
|
|
@@ -7386,7 +7524,7 @@ async function diffCommand(skill, opts = {}) {
|
|
|
7386
7524
|
lastSyncedHash: null,
|
|
7387
7525
|
files: files.map((f) => ({ path: f, status: "added", binary: false }))
|
|
7388
7526
|
}],
|
|
7389
|
-
next: [`
|
|
7527
|
+
next: [cliCmd(`push ${copy.path}`)]
|
|
7390
7528
|
});
|
|
7391
7529
|
return;
|
|
7392
7530
|
}
|
|
@@ -7398,7 +7536,7 @@ async function diffCommand(skill, opts = {}) {
|
|
|
7398
7536
|
log.blank();
|
|
7399
7537
|
log.info("Baseline: empty");
|
|
7400
7538
|
log.info(`Local copy: ${tildePath(copy.path)}`);
|
|
7401
|
-
printNext([{ command: `
|
|
7539
|
+
printNext([{ command: cliCmd(`push ${tildePath(copy.path)}`), description: `publish ${skill} as v1` }]);
|
|
7402
7540
|
return;
|
|
7403
7541
|
}
|
|
7404
7542
|
const localHash = await skillContentHash(copy.path) ?? "";
|
|
@@ -7417,7 +7555,7 @@ async function diffCommand(skill, opts = {}) {
|
|
|
7417
7555
|
lastSyncedHash: entry.hash,
|
|
7418
7556
|
files: []
|
|
7419
7557
|
}],
|
|
7420
|
-
next: ["
|
|
7558
|
+
next: [cliCmd("status")]
|
|
7421
7559
|
});
|
|
7422
7560
|
return;
|
|
7423
7561
|
}
|
|
@@ -7427,17 +7565,17 @@ async function diffCommand(skill, opts = {}) {
|
|
|
7427
7565
|
log.info(`Last synced at v${entry.version_seq}.`);
|
|
7428
7566
|
log.info(`Local copy: ${tildePath(copy.path)}`);
|
|
7429
7567
|
log.blank();
|
|
7430
|
-
log.info(
|
|
7431
|
-
printNext([{ command: "
|
|
7568
|
+
log.info(`Run ${cliCmd("status")} when back online for a full file-by-file comparison.`);
|
|
7569
|
+
printNext([{ command: cliCmd("status") }]);
|
|
7432
7570
|
return;
|
|
7433
7571
|
}
|
|
7434
7572
|
if (copies.length === 0) {
|
|
7435
7573
|
if (json) {
|
|
7436
|
-
emitJson({ skill, agent: agentArg ?? null, source: "library", library: { available: libraryAvailable, version: libraryVersion, hash: null }, agents: [], next: libraryAvailable ? ["
|
|
7574
|
+
emitJson({ skill, agent: agentArg ?? null, source: "library", library: { available: libraryAvailable, version: libraryVersion, hash: null }, agents: [], next: libraryAvailable ? [cliCmd("pull")] : [] });
|
|
7437
7575
|
} else {
|
|
7438
7576
|
log.info(`No local copy of ${skill} found on this machine.`);
|
|
7439
7577
|
if (libraryAvailable) {
|
|
7440
|
-
printNext([{ command: "
|
|
7578
|
+
printNext([{ command: cliCmd("pull"), description: `install ${skill} from the Library` }]);
|
|
7441
7579
|
} else {
|
|
7442
7580
|
log.err(`${skill} is not in the Library and has no local copy.`);
|
|
7443
7581
|
}
|
|
@@ -7493,7 +7631,7 @@ async function diffCommand(skill, opts = {}) {
|
|
|
7493
7631
|
source: libraryAvailable ? "library" : "last_synced_manifest",
|
|
7494
7632
|
library: { available: libraryAvailable, version: libraryVersion, hash: null },
|
|
7495
7633
|
agents: agentResults,
|
|
7496
|
-
next: ["
|
|
7634
|
+
next: [cliCmd("status")]
|
|
7497
7635
|
});
|
|
7498
7636
|
return;
|
|
7499
7637
|
}
|
|
@@ -7522,10 +7660,10 @@ async function diffCommand(skill, opts = {}) {
|
|
|
7522
7660
|
const conflict = agentResults.find((r) => r.state === "changed_in_two_places");
|
|
7523
7661
|
const next = [];
|
|
7524
7662
|
if (conflict) {
|
|
7525
|
-
next.push({ command: `
|
|
7526
|
-
next.push({ command: `
|
|
7663
|
+
next.push({ command: cliCmd(`pull ${skill} --agent ${conflict.agent}${conflict.scope === "project" ? " --project" : ""}`), description: "use the Library version (your local copy is backed up first)" });
|
|
7664
|
+
next.push({ command: cliCmd(`push ${tildePath(conflict.path)}`), description: "publish your local version" });
|
|
7527
7665
|
} else {
|
|
7528
|
-
next.push({ command: "
|
|
7666
|
+
next.push({ command: cliCmd("status") });
|
|
7529
7667
|
}
|
|
7530
7668
|
printNext(next);
|
|
7531
7669
|
void sep6;
|
|
@@ -7619,7 +7757,7 @@ async function restoreCommand(skill, opts = {}) {
|
|
|
7619
7757
|
wouldMutate: false,
|
|
7620
7758
|
selectedBackup: null,
|
|
7621
7759
|
message: `${all.length} backups available`,
|
|
7622
|
-
next: skill ? [`
|
|
7760
|
+
next: skill ? [cliCmd(`restore ${skill} --agent ${detected[0] ?? "claude"}`)] : []
|
|
7623
7761
|
});
|
|
7624
7762
|
return;
|
|
7625
7763
|
}
|
|
@@ -7641,12 +7779,12 @@ async function restoreCommand(skill, opts = {}) {
|
|
|
7641
7779
|
log.info(` ${b.skill.padEnd(16)}${b.timestamp.padEnd(24)}${tildePath(b.path)}`);
|
|
7642
7780
|
}
|
|
7643
7781
|
if (all[0]) {
|
|
7644
|
-
printNext([{ command: `
|
|
7782
|
+
printNext([{ command: cliCmd(`restore ${all[0].skill} --agent ${all[0].agent}`) }]);
|
|
7645
7783
|
}
|
|
7646
7784
|
return;
|
|
7647
7785
|
}
|
|
7648
7786
|
if (!skill) {
|
|
7649
|
-
if (!json) log.err("
|
|
7787
|
+
if (!json) log.err(`${cliCmd("restore")} needs a skill name, or use ${cliCmd("restore --list")} to see backups.`);
|
|
7650
7788
|
process.exitCode = 2;
|
|
7651
7789
|
return;
|
|
7652
7790
|
}
|
|
@@ -7664,10 +7802,10 @@ async function restoreCommand(skill, opts = {}) {
|
|
|
7664
7802
|
const backups = (await listBackups(searchAgents)).filter((b) => b.skill === skill);
|
|
7665
7803
|
if (backups.length === 0) {
|
|
7666
7804
|
if (json) {
|
|
7667
|
-
emitJson({ mode: "plan", applied: false, skill: { slug: skill }, agent, scope: null, availableBackups: [], wouldMutate: false, selectedBackup: null, message: `No backups found for ${skill}.`, next: ["
|
|
7805
|
+
emitJson({ mode: "plan", applied: false, skill: { slug: skill }, agent, scope: null, availableBackups: [], wouldMutate: false, selectedBackup: null, message: `No backups found for ${skill}.`, next: [cliCmd("restore --list")] });
|
|
7668
7806
|
} else {
|
|
7669
7807
|
log.err(`No backups found for ${skill}.`);
|
|
7670
|
-
printNext([{ command: "
|
|
7808
|
+
printNext([{ command: cliCmd("restore --list") }]);
|
|
7671
7809
|
}
|
|
7672
7810
|
process.exitCode = 2;
|
|
7673
7811
|
return;
|
|
@@ -7692,7 +7830,7 @@ async function restoreCommand(skill, opts = {}) {
|
|
|
7692
7830
|
}
|
|
7693
7831
|
}
|
|
7694
7832
|
if (opts.yes && !opts.dryRun && !agentArg && !isInteractive()) {
|
|
7695
|
-
if (!json) log.err("
|
|
7833
|
+
if (!json) log.err(`${cliCmd("restore")} needs --agent <name> --backup <timestamp> --yes for non-interactive restore.`);
|
|
7696
7834
|
process.exitCode = 2;
|
|
7697
7835
|
return;
|
|
7698
7836
|
}
|
|
@@ -7712,7 +7850,7 @@ async function restoreCommand(skill, opts = {}) {
|
|
|
7712
7850
|
wouldMutate: true,
|
|
7713
7851
|
selectedBackup: selected?.timestamp ?? null,
|
|
7714
7852
|
message: `${skill} would be restored from ${chosen.timestamp}`,
|
|
7715
|
-
next: [`
|
|
7853
|
+
next: [cliCmd(`restore ${skill} --agent ${agent} --backup ${chosen.timestamp} --yes`)]
|
|
7716
7854
|
});
|
|
7717
7855
|
} else {
|
|
7718
7856
|
log.heading("Restore plan");
|
|
@@ -7742,8 +7880,8 @@ async function restoreCommand(skill, opts = {}) {
|
|
|
7742
7880
|
}
|
|
7743
7881
|
selected = agentBackups[choice - 1];
|
|
7744
7882
|
} else {
|
|
7745
|
-
log.err(
|
|
7746
|
-
log.info(
|
|
7883
|
+
log.err(`${cliCmd("restore")} needs a backup timestamp. Pass --backup <timestamp>, or run interactively.`);
|
|
7884
|
+
log.info(`Run ${cliCmd("restore --list")} to see available backups.`);
|
|
7747
7885
|
process.exitCode = 2;
|
|
7748
7886
|
return;
|
|
7749
7887
|
}
|
|
@@ -7787,15 +7925,15 @@ async function restoreCommand(skill, opts = {}) {
|
|
|
7787
7925
|
preRestoreBackupPath: preRestoreBackup,
|
|
7788
7926
|
result: "restored",
|
|
7789
7927
|
message: `Restored ${skill} from ${selected.timestamp}`,
|
|
7790
|
-
next: [`
|
|
7928
|
+
next: [cliCmd(`status --agent ${agent}`)]
|
|
7791
7929
|
});
|
|
7792
7930
|
return;
|
|
7793
7931
|
}
|
|
7794
7932
|
log.ok(`Restored ${skill} from ${selected.timestamp}`);
|
|
7795
|
-
printNext([{ command: `
|
|
7933
|
+
printNext([{ command: cliCmd(`status --agent ${agent}`) }]);
|
|
7796
7934
|
} catch (error) {
|
|
7797
7935
|
if (json) {
|
|
7798
|
-
emitJson({ mode: "apply", applied: true, skill: { slug: skill }, agent, scope: selected.scope, backup: selected.timestamp, preRestoreBackupPath: null, result: "failed", message: error.message, next: ["
|
|
7936
|
+
emitJson({ mode: "apply", applied: true, skill: { slug: skill }, agent, scope: selected.scope, backup: selected.timestamp, preRestoreBackupPath: null, result: "failed", message: error.message, next: [cliCmd("restore --list")] });
|
|
7799
7937
|
} else {
|
|
7800
7938
|
log.err(error.message);
|
|
7801
7939
|
}
|
|
@@ -7821,7 +7959,7 @@ async function dashboardCommand() {
|
|
|
7821
7959
|
}
|
|
7822
7960
|
log.blank();
|
|
7823
7961
|
log.info("Not signed in \u2014 can't compare with your team Library.");
|
|
7824
|
-
printNext([{ command: "
|
|
7962
|
+
printNext([{ command: cliCmd("login"), description: "sign in to see and sync your Library" }]);
|
|
7825
7963
|
return;
|
|
7826
7964
|
}
|
|
7827
7965
|
const me = await fetchMe().catch(() => null);
|
|
@@ -7891,23 +8029,23 @@ async function dashboardCommand() {
|
|
|
7891
8029
|
}
|
|
7892
8030
|
const next = [];
|
|
7893
8031
|
if (anyLocalChanges && anyUpdates) {
|
|
7894
|
-
next.push({ command: "
|
|
7895
|
-
next.push({ command: "
|
|
7896
|
-
next.push({ command: "
|
|
8032
|
+
next.push({ command: cliCmd("sync"), description: "review local changes and Library updates together" });
|
|
8033
|
+
next.push({ command: cliCmd("push"), description: "publish local skills to the Library" });
|
|
8034
|
+
next.push({ command: cliCmd("pull"), description: "update agent copies from the Library" });
|
|
7897
8035
|
} else if (anyUpdates) {
|
|
7898
|
-
next.push({ command: "
|
|
8036
|
+
next.push({ command: cliCmd("pull"), description: "update agent copies from the Library" });
|
|
7899
8037
|
} else if (anyLocalChanges) {
|
|
7900
|
-
next.push({ command: "
|
|
8038
|
+
next.push({ command: cliCmd("push"), description: "publish local skills to the Library" });
|
|
7901
8039
|
} else if (!libResult.ok) {
|
|
7902
|
-
next.push({ command: "
|
|
8040
|
+
next.push({ command: cliCmd("status"), description: "retry full comparison" });
|
|
7903
8041
|
}
|
|
7904
|
-
next.push({ command: "
|
|
8042
|
+
next.push({ command: cliCmd("status"), description: "see full detail" });
|
|
7905
8043
|
printNext(next);
|
|
7906
8044
|
void [];
|
|
7907
8045
|
}
|
|
7908
8046
|
|
|
7909
8047
|
// src/lib/help.ts
|
|
7910
|
-
import
|
|
8048
|
+
import chalk7 from "chalk";
|
|
7911
8049
|
var GROUPS = [
|
|
7912
8050
|
{
|
|
7913
8051
|
title: "Sign in",
|
|
@@ -7977,21 +8115,21 @@ var COMMON_FLAGS = `Common flags
|
|
|
7977
8115
|
--no-secret-check with push, skip the pre-publish secret scan`;
|
|
7978
8116
|
function printGroupedHelp() {
|
|
7979
8117
|
const out2 = process.stdout;
|
|
7980
|
-
out2.write("\n" +
|
|
7981
|
-
out2.write(` ${
|
|
8118
|
+
out2.write("\n" + chalk7.bold("Usage: floom [command]") + "\n\n");
|
|
8119
|
+
out2.write(` ${chalk7.cyan.bold("floom")} your dashboard \u2014 Library and agent status at a glance
|
|
7982
8120
|
`);
|
|
7983
8121
|
const allNames = GROUPS.flatMap((g) => g.rows.map((r) => r.name));
|
|
7984
8122
|
const width = Math.max(...allNames.map((n) => n.length), 14);
|
|
7985
8123
|
for (const group of GROUPS) {
|
|
7986
|
-
out2.write("\n" +
|
|
8124
|
+
out2.write("\n" + chalk7.bold(group.title) + "\n");
|
|
7987
8125
|
for (const row of group.rows) {
|
|
7988
|
-
const aliasNote = row.alias ?
|
|
7989
|
-
out2.write(` ${
|
|
8126
|
+
const aliasNote = row.alias ? chalk7.dim(` ${row.alias} (alias)`) : "";
|
|
8127
|
+
out2.write(` ${chalk7.cyan.bold(row.name.padEnd(width + 2))}${row.description}${aliasNote}
|
|
7990
8128
|
`);
|
|
7991
8129
|
}
|
|
7992
8130
|
}
|
|
7993
8131
|
out2.write("\n" + COMMON_FLAGS + "\n");
|
|
7994
|
-
out2.write("\n" +
|
|
8132
|
+
out2.write("\n" + chalk7.dim("More help: https://floom.dev/docs") + "\n");
|
|
7995
8133
|
}
|
|
7996
8134
|
|
|
7997
8135
|
// src/index.ts
|
|
@@ -8054,7 +8192,7 @@ function editDistance(a, b) {
|
|
|
8054
8192
|
return curr[b.length];
|
|
8055
8193
|
}
|
|
8056
8194
|
helpOpt(program.command("login").description("sign in to your Floom workspace")).action(loginCommand);
|
|
8057
|
-
helpOpt(program.command("logout").description("sign out on this machine")).action(logoutCommand);
|
|
8195
|
+
helpOpt(program.command("logout").description("sign out on this machine").option("--all", "sign out all CLI sessions for this account")).action((opts) => logoutCommand(opts));
|
|
8058
8196
|
helpOpt(program.command("account").description("show account details").option("--json", "print account state as JSON")).action((opts) => accountCommand(opts));
|
|
8059
8197
|
helpOpt(program.command("whoami").description("show account details (alias)").option("--json", "print account state as JSON")).action((opts) => {
|
|
8060
8198
|
aliasNotice("whoami", "account");
|