@braid-cloud/cli 0.1.0 → 0.1.1
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/LICENSE +18 -0
- package/README.md +174 -177
- package/dist/index.js +493 -176
- package/dist/index.js.map +1 -1
- package/package.json +64 -57
package/dist/index.js
CHANGED
|
@@ -56,12 +56,12 @@ import { homedir } from "os";
|
|
|
56
56
|
import { dirname, join, parse } from "path";
|
|
57
57
|
import process2 from "process";
|
|
58
58
|
import { Data, Effect, pipe } from "effect";
|
|
59
|
-
var CONFIG_DIR, CONFIG_FILE, PROJECT_CONFIG_FILENAME, USER_CONFIG_FILENAME, ConfigReadError, ConfigWriteError, findConfigFile, findProjectConfigFile, findUserConfigFile, loadProjectConfig, loadUserConfig, resolveServerUrlFromConfig, applyConfigSource, applyEnvOverrides, createDefaultMergedConfig, loadMergedConfig, loadConfig, saveConfig, getApiKey, setApiKey, getServerUrl, clearApiKey, loadConfigAsync, loadProjectConfigAsync, loadUserConfigAsync, loadMergedConfigAsync, findProjectConfigFileAsync, findUserConfigFileAsync, saveConfigAsync, getApiKeyAsync, setApiKeyAsync, getServerUrlAsync, clearApiKeyAsync;
|
|
59
|
+
var CONFIG_DIR, CONFIG_FILE, PROJECT_CONFIG_FILENAME, USER_CONFIG_FILENAME, ConfigReadError, ConfigWriteError, findConfigFile, findProjectConfigFile, findUserConfigFile, loadProjectConfig, loadUserConfig, isValidServerUrl, resolveServerUrlFromConfig, applyConfigSource, applyEnvOverrides, createDefaultMergedConfig, loadMergedConfig, loadConfig, saveConfig, getApiKey, setApiKey, getServerUrl, clearApiKey, loadConfigAsync, loadProjectConfigAsync, loadUserConfigAsync, loadMergedConfigAsync, findProjectConfigFileAsync, findUserConfigFileAsync, saveConfigAsync, getApiKeyAsync, setApiKeyAsync, getServerUrlAsync, clearApiKeyAsync;
|
|
60
60
|
var init_config = __esm({
|
|
61
61
|
"src/lib/config.ts"() {
|
|
62
62
|
"use strict";
|
|
63
63
|
init_esm_shims();
|
|
64
|
-
CONFIG_DIR = join(homedir(), ".config", "braid
|
|
64
|
+
CONFIG_DIR = join(homedir(), ".config", "braid");
|
|
65
65
|
CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
66
66
|
PROJECT_CONFIG_FILENAME = "braid.json";
|
|
67
67
|
USER_CONFIG_FILENAME = "braid.user.json";
|
|
@@ -107,11 +107,23 @@ var init_config = __esm({
|
|
|
107
107
|
},
|
|
108
108
|
catch: () => void 0
|
|
109
109
|
}).pipe(Effect.orElseSucceed(() => void 0));
|
|
110
|
+
isValidServerUrl = (url) => {
|
|
111
|
+
try {
|
|
112
|
+
const parsed = new URL(url);
|
|
113
|
+
return parsed.protocol === "https:" || parsed.protocol === "http:";
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
110
118
|
resolveServerUrlFromConfig = (config) => {
|
|
111
119
|
if (!config) {
|
|
112
120
|
return void 0;
|
|
113
121
|
}
|
|
114
|
-
|
|
122
|
+
const url = config.skills?.serverUrl ?? config.serverUrl;
|
|
123
|
+
if (url && !isValidServerUrl(url)) {
|
|
124
|
+
return void 0;
|
|
125
|
+
}
|
|
126
|
+
return url;
|
|
115
127
|
};
|
|
116
128
|
applyConfigSource = (merged, config) => {
|
|
117
129
|
if (!config) {
|
|
@@ -130,11 +142,11 @@ var init_config = __esm({
|
|
|
130
142
|
if (config.personalProjects) {
|
|
131
143
|
merged.personalProjects = config.personalProjects;
|
|
132
144
|
}
|
|
133
|
-
if (config.
|
|
134
|
-
merged.
|
|
145
|
+
if (config.includeUserGlobal !== void 0) {
|
|
146
|
+
merged.includeUserGlobal = config.includeUserGlobal;
|
|
135
147
|
}
|
|
136
|
-
if (config.
|
|
137
|
-
merged.
|
|
148
|
+
if (config.includeOrgGlobal !== void 0) {
|
|
149
|
+
merged.includeOrgGlobal = config.includeOrgGlobal;
|
|
138
150
|
}
|
|
139
151
|
if (config.agents) {
|
|
140
152
|
merged.agents = config.agents;
|
|
@@ -148,14 +160,14 @@ var init_config = __esm({
|
|
|
148
160
|
merged.token = process2.env.BRAID_API_KEY;
|
|
149
161
|
}
|
|
150
162
|
const envServerUrl = process2.env.BRAID_SKILLS_SERVER_URL ?? process2.env.BRAID_SERVER_URL;
|
|
151
|
-
if (envServerUrl) {
|
|
163
|
+
if (envServerUrl && isValidServerUrl(envServerUrl)) {
|
|
152
164
|
merged.serverUrl = envServerUrl;
|
|
153
165
|
}
|
|
154
166
|
};
|
|
155
167
|
createDefaultMergedConfig = () => ({
|
|
156
168
|
serverUrl: "https://braid.cloud",
|
|
157
|
-
|
|
158
|
-
|
|
169
|
+
includeUserGlobal: true,
|
|
170
|
+
includeOrgGlobal: true
|
|
159
171
|
});
|
|
160
172
|
loadMergedConfig = () => pipe(
|
|
161
173
|
Effect.all({
|
|
@@ -192,8 +204,11 @@ var init_config = __esm({
|
|
|
192
204
|
saveConfig = (config) => pipe(
|
|
193
205
|
Effect.tryPromise({
|
|
194
206
|
try: async () => {
|
|
195
|
-
await mkdir(dirname(CONFIG_FILE), { recursive: true });
|
|
196
|
-
await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2),
|
|
207
|
+
await mkdir(dirname(CONFIG_FILE), { recursive: true, mode: 448 });
|
|
208
|
+
await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), {
|
|
209
|
+
encoding: "utf-8",
|
|
210
|
+
mode: 384
|
|
211
|
+
});
|
|
197
212
|
},
|
|
198
213
|
catch: (e) => new ConfigWriteError({ path: CONFIG_FILE, cause: e })
|
|
199
214
|
})
|
|
@@ -347,17 +362,14 @@ var buildExportUrl = (serverUrl, options) => {
|
|
|
347
362
|
options.personalProjects.join(",")
|
|
348
363
|
);
|
|
349
364
|
}
|
|
350
|
-
if (options.
|
|
365
|
+
if (options.includeUserGlobal !== void 0) {
|
|
351
366
|
url.searchParams.set(
|
|
352
367
|
"includeUserGlobal",
|
|
353
|
-
String(options.
|
|
368
|
+
String(options.includeUserGlobal)
|
|
354
369
|
);
|
|
355
370
|
}
|
|
356
|
-
if (options.
|
|
357
|
-
url.searchParams.set(
|
|
358
|
-
"includeOrgGlobal",
|
|
359
|
-
String(options.includeOrgGlobalRules)
|
|
360
|
-
);
|
|
371
|
+
if (options.includeOrgGlobal !== void 0) {
|
|
372
|
+
url.searchParams.set("includeOrgGlobal", String(options.includeOrgGlobal));
|
|
361
373
|
}
|
|
362
374
|
return url;
|
|
363
375
|
};
|
|
@@ -422,7 +434,7 @@ var validateApiKeyAsync = (apiKey, serverUrl) => Effect2.runPromise(validateApiK
|
|
|
422
434
|
// src/commands/auth.ts
|
|
423
435
|
init_config();
|
|
424
436
|
async function authCommand(options) {
|
|
425
|
-
intro("braid
|
|
437
|
+
intro("braid auth");
|
|
426
438
|
const config = await loadMergedConfigAsync();
|
|
427
439
|
if (config.token) {
|
|
428
440
|
const shouldReplace = await confirm({
|
|
@@ -466,7 +478,7 @@ async function authCommand(options) {
|
|
|
466
478
|
authSpinner.stop("API key validated and saved");
|
|
467
479
|
log.success(`Config saved to ${CONFIG_FILE}`);
|
|
468
480
|
outro(
|
|
469
|
-
"You're authenticated! Run 'braid
|
|
481
|
+
"You're authenticated! Run 'braid install --profile <name>' to install skills."
|
|
470
482
|
);
|
|
471
483
|
} catch (error) {
|
|
472
484
|
authSpinner.stop("Validation failed");
|
|
@@ -514,9 +526,7 @@ function displayResolvedSettings(config) {
|
|
|
514
526
|
async function authStatusCommand() {
|
|
515
527
|
const config = await loadMergedConfigAsync();
|
|
516
528
|
if (!config.token) {
|
|
517
|
-
log.warn(
|
|
518
|
-
"Not authenticated. Run 'braid-skills auth' to configure your API key."
|
|
519
|
-
);
|
|
529
|
+
log.warn("Not authenticated. Run 'braid auth' to configure your API key.");
|
|
520
530
|
log.info(
|
|
521
531
|
`Or create a ${USER_CONFIG_FILENAME} file with: { "token": "br_xxx" }`
|
|
522
532
|
);
|
|
@@ -567,7 +577,10 @@ var AGENTS = [
|
|
|
567
577
|
id: "claude-code",
|
|
568
578
|
name: "Claude Code",
|
|
569
579
|
projectPath: ".claude/skills",
|
|
570
|
-
globalPath: join2(home, ".claude", "skills")
|
|
580
|
+
globalPath: join2(home, ".claude", "skills"),
|
|
581
|
+
rulesProjectPath: ".claude/rules",
|
|
582
|
+
rulesGlobalPath: join2(home, ".claude", "rules"),
|
|
583
|
+
ruleFormat: "markdown-dir"
|
|
571
584
|
},
|
|
572
585
|
{
|
|
573
586
|
id: "moltbot",
|
|
@@ -579,7 +592,9 @@ var AGENTS = [
|
|
|
579
592
|
id: "cline",
|
|
580
593
|
name: "Cline",
|
|
581
594
|
projectPath: ".cline/skills",
|
|
582
|
-
globalPath: join2(home, ".cline", "skills")
|
|
595
|
+
globalPath: join2(home, ".cline", "skills"),
|
|
596
|
+
rulesProjectPath: ".clinerules",
|
|
597
|
+
ruleFormat: "append-single"
|
|
583
598
|
},
|
|
584
599
|
{
|
|
585
600
|
id: "codebuddy",
|
|
@@ -615,7 +630,9 @@ var AGENTS = [
|
|
|
615
630
|
id: "cursor",
|
|
616
631
|
name: "Cursor",
|
|
617
632
|
projectPath: ".cursor/skills",
|
|
618
|
-
globalPath: join2(home, ".cursor", "skills")
|
|
633
|
+
globalPath: join2(home, ".cursor", "skills"),
|
|
634
|
+
rulesProjectPath: ".cursor/rules",
|
|
635
|
+
ruleFormat: "mdc"
|
|
619
636
|
},
|
|
620
637
|
{
|
|
621
638
|
id: "droid",
|
|
@@ -633,7 +650,9 @@ var AGENTS = [
|
|
|
633
650
|
id: "github-copilot",
|
|
634
651
|
name: "GitHub Copilot",
|
|
635
652
|
projectPath: ".github/skills",
|
|
636
|
-
globalPath: join2(home, ".copilot", "skills")
|
|
653
|
+
globalPath: join2(home, ".copilot", "skills"),
|
|
654
|
+
rulesProjectPath: ".github/copilot-instructions.md",
|
|
655
|
+
ruleFormat: "append-single"
|
|
637
656
|
},
|
|
638
657
|
{
|
|
639
658
|
id: "goose",
|
|
@@ -711,7 +730,10 @@ var AGENTS = [
|
|
|
711
730
|
id: "roo",
|
|
712
731
|
name: "Roo Code",
|
|
713
732
|
projectPath: ".roo/skills",
|
|
714
|
-
globalPath: join2(home, ".roo", "skills")
|
|
733
|
+
globalPath: join2(home, ".roo", "skills"),
|
|
734
|
+
rulesProjectPath: ".roo/rules",
|
|
735
|
+
rulesGlobalPath: join2(home, ".roo", "rules"),
|
|
736
|
+
ruleFormat: "markdown-dir"
|
|
715
737
|
},
|
|
716
738
|
{
|
|
717
739
|
id: "trae",
|
|
@@ -723,7 +745,9 @@ var AGENTS = [
|
|
|
723
745
|
id: "windsurf",
|
|
724
746
|
name: "Windsurf",
|
|
725
747
|
projectPath: ".windsurf/skills",
|
|
726
|
-
globalPath: join2(home, ".codeium", "windsurf", "skills")
|
|
748
|
+
globalPath: join2(home, ".codeium", "windsurf", "skills"),
|
|
749
|
+
rulesProjectPath: ".windsurfrules",
|
|
750
|
+
ruleFormat: "append-single"
|
|
727
751
|
},
|
|
728
752
|
{
|
|
729
753
|
id: "zencoder",
|
|
@@ -742,6 +766,15 @@ var AGENTS = [
|
|
|
742
766
|
name: "Pochi",
|
|
743
767
|
projectPath: ".pochi/skills",
|
|
744
768
|
globalPath: join2(home, ".pochi", "skills")
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
id: "zed",
|
|
772
|
+
name: "Zed",
|
|
773
|
+
projectPath: "",
|
|
774
|
+
globalPath: "",
|
|
775
|
+
rulesProjectPath: ".zed/rules",
|
|
776
|
+
rulesGlobalPath: join2(home, ".config", "zed", "rules"),
|
|
777
|
+
ruleFormat: "markdown-dir"
|
|
745
778
|
}
|
|
746
779
|
];
|
|
747
780
|
var directoryExists = (path2) => pipe3(
|
|
@@ -752,10 +785,6 @@ var directoryExists = (path2) => pipe3(
|
|
|
752
785
|
Effect3.map(() => true),
|
|
753
786
|
Effect3.orElseSucceed(() => false)
|
|
754
787
|
);
|
|
755
|
-
var getAgentConfigDir = (skillsPath) => {
|
|
756
|
-
const parts = skillsPath.split("/");
|
|
757
|
-
return parts.slice(0, -1).join("/");
|
|
758
|
-
};
|
|
759
788
|
var detectAgents = (projectRoot) => {
|
|
760
789
|
const cwd = projectRoot ?? process4.cwd();
|
|
761
790
|
return pipe3(
|
|
@@ -763,19 +792,25 @@ var detectAgents = (projectRoot) => {
|
|
|
763
792
|
AGENTS,
|
|
764
793
|
(agent) => pipe3(
|
|
765
794
|
Effect3.all({
|
|
766
|
-
hasProjectConfig: directoryExists(
|
|
767
|
-
|
|
768
|
-
),
|
|
769
|
-
|
|
795
|
+
hasProjectConfig: agent.projectPath ? directoryExists(join2(cwd, agent.projectPath)) : Effect3.succeed(false),
|
|
796
|
+
hasGlobalConfig: agent.globalPath ? directoryExists(agent.globalPath) : Effect3.succeed(false),
|
|
797
|
+
hasRulesProjectConfig: agent.rulesProjectPath ? directoryExists(join2(cwd, agent.rulesProjectPath)) : Effect3.succeed(false),
|
|
798
|
+
hasRulesGlobalConfig: agent.rulesGlobalPath ? directoryExists(agent.rulesGlobalPath) : Effect3.succeed(false)
|
|
770
799
|
}),
|
|
771
800
|
Effect3.map(
|
|
772
|
-
({
|
|
773
|
-
...agent,
|
|
801
|
+
({
|
|
774
802
|
hasProjectConfig,
|
|
775
|
-
hasGlobalConfig
|
|
803
|
+
hasGlobalConfig,
|
|
804
|
+
hasRulesProjectConfig,
|
|
805
|
+
hasRulesGlobalConfig
|
|
806
|
+
}) => ({
|
|
807
|
+
...agent,
|
|
808
|
+
hasProjectConfig: hasProjectConfig || hasRulesProjectConfig,
|
|
809
|
+
hasGlobalConfig: hasGlobalConfig || hasRulesGlobalConfig
|
|
776
810
|
})
|
|
777
811
|
)
|
|
778
|
-
)
|
|
812
|
+
),
|
|
813
|
+
{ concurrency: "unbounded" }
|
|
779
814
|
),
|
|
780
815
|
Effect3.map(
|
|
781
816
|
(agents) => agents.filter((a) => a.hasProjectConfig || a.hasGlobalConfig)
|
|
@@ -787,11 +822,24 @@ var detectAgentsAsync = (projectRoot) => Effect3.runPromise(detectAgents(project
|
|
|
787
822
|
var directoryExistsAsync = (path2) => Effect3.runPromise(directoryExists(path2));
|
|
788
823
|
var resolveInstallPath = (agent, options) => {
|
|
789
824
|
if (options.global) {
|
|
790
|
-
return agent.globalPath;
|
|
825
|
+
return agent.globalPath || void 0;
|
|
826
|
+
}
|
|
827
|
+
if (!agent.projectPath) {
|
|
828
|
+
return void 0;
|
|
791
829
|
}
|
|
792
830
|
const cwd = options.projectRoot ?? process4.cwd();
|
|
793
831
|
return join2(cwd, agent.projectPath);
|
|
794
832
|
};
|
|
833
|
+
var resolveRulesInstallPath = (agent, options) => {
|
|
834
|
+
if (options.global) {
|
|
835
|
+
return agent.rulesGlobalPath;
|
|
836
|
+
}
|
|
837
|
+
if (!agent.rulesProjectPath) {
|
|
838
|
+
return void 0;
|
|
839
|
+
}
|
|
840
|
+
const cwd = options.projectRoot ?? process4.cwd();
|
|
841
|
+
return join2(cwd, agent.rulesProjectPath);
|
|
842
|
+
};
|
|
795
843
|
|
|
796
844
|
// src/commands/install.ts
|
|
797
845
|
init_config();
|
|
@@ -867,12 +915,204 @@ var readMetadataAsync = (skillsDir) => Effect4.runPromise(readMetadata(skillsDir
|
|
|
867
915
|
var updateMetadataAsync = (skillsDir, newSkills) => Effect4.runPromise(updateMetadata(skillsDir, newSkills));
|
|
868
916
|
var removeFromMetadataAsync = (skillsDir, skillName) => Effect4.runPromise(removeFromMetadata(skillsDir, skillName));
|
|
869
917
|
|
|
870
|
-
// src/lib/
|
|
918
|
+
// src/lib/rule-writer.ts
|
|
871
919
|
init_esm_shims();
|
|
872
|
-
import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
|
|
873
|
-
import { dirname as dirname2,
|
|
920
|
+
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
921
|
+
import { dirname as dirname2, resolve, sep } from "path";
|
|
874
922
|
import { Data as Data4, Effect as Effect5, pipe as pipe5 } from "effect";
|
|
875
|
-
var
|
|
923
|
+
var RuleWriteError = class extends Data4.TaggedError("RuleWriteError") {
|
|
924
|
+
};
|
|
925
|
+
var createDirectory = (dir) => Effect5.tryPromise({
|
|
926
|
+
try: () => mkdir2(dir, { recursive: true }),
|
|
927
|
+
catch: (e) => new RuleWriteError({ path: dir, operation: "mkdir", cause: e })
|
|
928
|
+
});
|
|
929
|
+
var writeTextFile = (fullPath, content) => Effect5.tryPromise({
|
|
930
|
+
try: () => writeFile3(fullPath, content, "utf-8"),
|
|
931
|
+
catch: (e) => new RuleWriteError({ path: fullPath, operation: "write", cause: e })
|
|
932
|
+
});
|
|
933
|
+
var readTextFile = (fullPath) => Effect5.tryPromise({
|
|
934
|
+
try: () => readFile3(fullPath, "utf-8"),
|
|
935
|
+
catch: (e) => new RuleWriteError({ path: fullPath, operation: "read", cause: e })
|
|
936
|
+
});
|
|
937
|
+
var assertRulePathWithinBase = (basePath, ruleName) => {
|
|
938
|
+
const resolvedBase = resolve(basePath);
|
|
939
|
+
const resolvedFull = resolve(basePath, ruleName);
|
|
940
|
+
if (resolvedFull !== resolvedBase && !resolvedFull.startsWith(resolvedBase + sep)) {
|
|
941
|
+
return Effect5.fail(
|
|
942
|
+
new RuleWriteError({
|
|
943
|
+
path: ruleName,
|
|
944
|
+
operation: "write",
|
|
945
|
+
cause: new Error(
|
|
946
|
+
`Path traversal detected: "${ruleName}" resolves outside base directory`
|
|
947
|
+
)
|
|
948
|
+
})
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
return Effect5.succeed(resolvedFull);
|
|
952
|
+
};
|
|
953
|
+
var BRAID_SECTION_START = "<!-- braid:rules:start -->";
|
|
954
|
+
var BRAID_SECTION_END = "<!-- braid:rules:end -->";
|
|
955
|
+
var YAML_SPECIAL_CHARS = /[\n\r:#{}[\],&*?|>!'"%@`]/;
|
|
956
|
+
function escapeYamlValue(value) {
|
|
957
|
+
if (YAML_SPECIAL_CHARS.test(value) || value.startsWith(" ") || value.endsWith(" ")) {
|
|
958
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
959
|
+
}
|
|
960
|
+
return value;
|
|
961
|
+
}
|
|
962
|
+
function buildMdcContent(rule) {
|
|
963
|
+
const lines = ["---"];
|
|
964
|
+
lines.push(`description: ${escapeYamlValue(rule.title)}`);
|
|
965
|
+
lines.push("alwaysApply: true");
|
|
966
|
+
lines.push("---");
|
|
967
|
+
lines.push("");
|
|
968
|
+
lines.push(`# ${rule.title}`);
|
|
969
|
+
lines.push("");
|
|
970
|
+
lines.push(rule.content);
|
|
971
|
+
return lines.join("\n");
|
|
972
|
+
}
|
|
973
|
+
function buildAppendContent(rules) {
|
|
974
|
+
const lines = [BRAID_SECTION_START, ""];
|
|
975
|
+
for (const rule of rules) {
|
|
976
|
+
lines.push(`## ${rule.title}`);
|
|
977
|
+
lines.push("");
|
|
978
|
+
lines.push(rule.content);
|
|
979
|
+
lines.push("");
|
|
980
|
+
}
|
|
981
|
+
lines.push(BRAID_SECTION_END);
|
|
982
|
+
return lines.join("\n");
|
|
983
|
+
}
|
|
984
|
+
var writeMdcRules = (basePath, rules) => pipe5(
|
|
985
|
+
createDirectory(basePath),
|
|
986
|
+
Effect5.flatMap(
|
|
987
|
+
() => Effect5.forEach(
|
|
988
|
+
rules,
|
|
989
|
+
(rule) => pipe5(
|
|
990
|
+
assertRulePathWithinBase(basePath, `${rule.name}.mdc`),
|
|
991
|
+
Effect5.flatMap(
|
|
992
|
+
(filePath) => writeTextFile(filePath, buildMdcContent(rule))
|
|
993
|
+
)
|
|
994
|
+
),
|
|
995
|
+
{ concurrency: "unbounded" }
|
|
996
|
+
)
|
|
997
|
+
),
|
|
998
|
+
Effect5.asVoid
|
|
999
|
+
);
|
|
1000
|
+
var writeMarkdownDirRules = (basePath, rules) => pipe5(
|
|
1001
|
+
createDirectory(basePath),
|
|
1002
|
+
Effect5.flatMap(
|
|
1003
|
+
() => Effect5.forEach(
|
|
1004
|
+
rules,
|
|
1005
|
+
(rule) => pipe5(
|
|
1006
|
+
assertRulePathWithinBase(basePath, `${rule.name}.md`),
|
|
1007
|
+
Effect5.flatMap(
|
|
1008
|
+
(filePath) => writeTextFile(filePath, `# ${rule.title}
|
|
1009
|
+
|
|
1010
|
+
${rule.content}`)
|
|
1011
|
+
)
|
|
1012
|
+
),
|
|
1013
|
+
{ concurrency: "unbounded" }
|
|
1014
|
+
)
|
|
1015
|
+
),
|
|
1016
|
+
Effect5.asVoid
|
|
1017
|
+
);
|
|
1018
|
+
var writeAppendSingleRules = (filePath, rules) => pipe5(
|
|
1019
|
+
createDirectory(dirname2(filePath)),
|
|
1020
|
+
Effect5.flatMap(
|
|
1021
|
+
() => pipe5(
|
|
1022
|
+
readTextFile(filePath),
|
|
1023
|
+
Effect5.orElseSucceed(() => "")
|
|
1024
|
+
)
|
|
1025
|
+
),
|
|
1026
|
+
Effect5.flatMap((existing) => {
|
|
1027
|
+
const braidContent = buildAppendContent(rules);
|
|
1028
|
+
const startIdx = existing.indexOf(BRAID_SECTION_START);
|
|
1029
|
+
const endIdx = existing.indexOf(BRAID_SECTION_END);
|
|
1030
|
+
let newContent;
|
|
1031
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
1032
|
+
newContent = existing.slice(0, startIdx) + braidContent + existing.slice(endIdx + BRAID_SECTION_END.length);
|
|
1033
|
+
} else {
|
|
1034
|
+
newContent = existing ? `${existing}
|
|
1035
|
+
|
|
1036
|
+
${braidContent}` : braidContent;
|
|
1037
|
+
}
|
|
1038
|
+
return writeTextFile(filePath, newContent);
|
|
1039
|
+
})
|
|
1040
|
+
);
|
|
1041
|
+
var writeRulesForFormat = (basePath, rules, format) => {
|
|
1042
|
+
switch (format) {
|
|
1043
|
+
case "mdc":
|
|
1044
|
+
return writeMdcRules(basePath, rules);
|
|
1045
|
+
case "markdown-dir":
|
|
1046
|
+
return writeMarkdownDirRules(basePath, rules);
|
|
1047
|
+
case "append-single":
|
|
1048
|
+
return writeAppendSingleRules(basePath, rules);
|
|
1049
|
+
default:
|
|
1050
|
+
return Effect5.void;
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
var writeRulesForAgent = (agent, rules, rulesPath) => {
|
|
1054
|
+
if (!agent.ruleFormat || rules.length === 0) {
|
|
1055
|
+
return Effect5.succeed({ written: 0, errors: [] });
|
|
1056
|
+
}
|
|
1057
|
+
return pipe5(
|
|
1058
|
+
writeRulesForFormat(rulesPath, rules, agent.ruleFormat),
|
|
1059
|
+
Effect5.map(() => ({
|
|
1060
|
+
written: rules.length,
|
|
1061
|
+
errors: []
|
|
1062
|
+
})),
|
|
1063
|
+
Effect5.catchAll(
|
|
1064
|
+
(error) => Effect5.succeed({
|
|
1065
|
+
written: 0,
|
|
1066
|
+
errors: [
|
|
1067
|
+
{
|
|
1068
|
+
agent: agent.name,
|
|
1069
|
+
error: error.cause instanceof Error ? error.cause.message : String(error.cause)
|
|
1070
|
+
}
|
|
1071
|
+
]
|
|
1072
|
+
})
|
|
1073
|
+
)
|
|
1074
|
+
);
|
|
1075
|
+
};
|
|
1076
|
+
var writeRulesForAgentAsync = (agent, rules, rulesPath) => Effect5.runPromise(writeRulesForAgent(agent, rules, rulesPath));
|
|
1077
|
+
|
|
1078
|
+
// src/lib/skill-writer.ts
|
|
1079
|
+
init_esm_shims();
|
|
1080
|
+
import { chmod, mkdir as mkdir3, writeFile as writeFile4 } from "fs/promises";
|
|
1081
|
+
import { dirname as dirname3, resolve as resolve2, sep as sep2 } from "path";
|
|
1082
|
+
import { Data as Data5, Effect as Effect6, pipe as pipe6 } from "effect";
|
|
1083
|
+
var WriteError = class extends Data5.TaggedError("WriteError") {
|
|
1084
|
+
};
|
|
1085
|
+
var createDirectory2 = (dir, fullPath) => Effect6.tryPromise({
|
|
1086
|
+
try: () => mkdir3(dir, { recursive: true }),
|
|
1087
|
+
catch: (e) => new WriteError({ path: fullPath, operation: "mkdir", cause: e })
|
|
1088
|
+
});
|
|
1089
|
+
var writeTextFile2 = (fullPath, content) => Effect6.tryPromise({
|
|
1090
|
+
try: () => writeFile4(fullPath, content, "utf-8"),
|
|
1091
|
+
catch: (e) => new WriteError({ path: fullPath, operation: "write", cause: e })
|
|
1092
|
+
});
|
|
1093
|
+
var writeBinaryFile = (fullPath, content) => Effect6.tryPromise({
|
|
1094
|
+
try: () => writeFile4(fullPath, content),
|
|
1095
|
+
catch: (e) => new WriteError({ path: fullPath, operation: "write", cause: e })
|
|
1096
|
+
});
|
|
1097
|
+
var makeExecutable = (fullPath) => Effect6.tryPromise({
|
|
1098
|
+
try: () => chmod(fullPath, 493),
|
|
1099
|
+
catch: (e) => new WriteError({ path: fullPath, operation: "chmod", cause: e })
|
|
1100
|
+
});
|
|
1101
|
+
var assertWithinBase = (basePath, untrustedPath) => {
|
|
1102
|
+
const resolvedBase = resolve2(basePath);
|
|
1103
|
+
const resolvedFull = resolve2(basePath, untrustedPath);
|
|
1104
|
+
if (resolvedFull !== resolvedBase && !resolvedFull.startsWith(resolvedBase + sep2)) {
|
|
1105
|
+
return Effect6.fail(
|
|
1106
|
+
new WriteError({
|
|
1107
|
+
path: untrustedPath,
|
|
1108
|
+
operation: "write",
|
|
1109
|
+
cause: new Error(
|
|
1110
|
+
`Path traversal detected: "${untrustedPath}" resolves outside base directory`
|
|
1111
|
+
)
|
|
1112
|
+
})
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
return Effect6.succeed(resolvedFull);
|
|
876
1116
|
};
|
|
877
1117
|
var COMPATIBILITY_REGEX = /^compatibility:\s*.+$/m;
|
|
878
1118
|
var rewriteCompatibility = (content, agentId) => {
|
|
@@ -908,42 +1148,48 @@ var isBinaryFile = (path2) => {
|
|
|
908
1148
|
];
|
|
909
1149
|
return binaryExtensions.some((ext) => path2.toLowerCase().endsWith(ext));
|
|
910
1150
|
};
|
|
911
|
-
var
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1151
|
+
var isScriptFile = (path2) => {
|
|
1152
|
+
const scriptExtensions = [".sh", ".bash", ".py", ".js", ".mjs", ".rb"];
|
|
1153
|
+
const lowerPath = path2.toLowerCase();
|
|
1154
|
+
const inScriptsDir = lowerPath.includes("/scripts/") || lowerPath.startsWith("scripts/");
|
|
1155
|
+
return inScriptsDir && scriptExtensions.some((ext) => lowerPath.endsWith(ext));
|
|
1156
|
+
};
|
|
1157
|
+
var writeFileContent = (fullPath, file, agentId) => isBinaryFile(file.path) ? writeBinaryFile(fullPath, decodeFileContentBinary(file)) : writeTextFile2(fullPath, decodeFileContent(file, agentId));
|
|
1158
|
+
var setExecutableIfScript = (fullPath, filePath) => isScriptFile(filePath) ? makeExecutable(fullPath) : Effect6.void;
|
|
1159
|
+
var writeSkillFile = (basePath, file, agentId) => pipe6(
|
|
1160
|
+
assertWithinBase(basePath, file.path),
|
|
1161
|
+
Effect6.flatMap((fullPath) => {
|
|
1162
|
+
const dir = dirname3(fullPath);
|
|
1163
|
+
return pipe6(
|
|
1164
|
+
createDirectory2(dir, fullPath),
|
|
1165
|
+
Effect6.flatMap(() => writeFileContent(fullPath, file, agentId)),
|
|
1166
|
+
Effect6.flatMap(() => setExecutableIfScript(fullPath, file.path))
|
|
1167
|
+
);
|
|
924
1168
|
})
|
|
925
1169
|
);
|
|
926
|
-
var writeSkill = (basePath, skill, agentId) =>
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1170
|
+
var writeSkill = (basePath, skill, agentId) => pipe6(
|
|
1171
|
+
assertWithinBase(basePath, skill.name),
|
|
1172
|
+
Effect6.flatMap(
|
|
1173
|
+
(skillDir) => pipe6(
|
|
1174
|
+
Effect6.forEach(
|
|
1175
|
+
skill.files,
|
|
1176
|
+
(file) => writeSkillFile(skillDir, file, agentId),
|
|
1177
|
+
{
|
|
1178
|
+
concurrency: "unbounded"
|
|
1179
|
+
}
|
|
1180
|
+
),
|
|
1181
|
+
Effect6.map(() => void 0)
|
|
1182
|
+
)
|
|
1183
|
+
)
|
|
1184
|
+
);
|
|
1185
|
+
var writeSkills = (basePath, skills, agentId) => pipe6(
|
|
1186
|
+
Effect6.forEach(
|
|
941
1187
|
skills,
|
|
942
|
-
(skill) =>
|
|
1188
|
+
(skill) => pipe6(
|
|
943
1189
|
writeSkill(basePath, skill, agentId),
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
(error) =>
|
|
1190
|
+
Effect6.map(() => ({ success: true, skill: skill.name })),
|
|
1191
|
+
Effect6.catchAll(
|
|
1192
|
+
(error) => Effect6.succeed({
|
|
947
1193
|
success: false,
|
|
948
1194
|
skill: skill.name,
|
|
949
1195
|
error: error.cause instanceof Error ? error.cause.message : String(error.cause)
|
|
@@ -952,14 +1198,14 @@ var writeSkills = (basePath, skills, agentId) => pipe5(
|
|
|
952
1198
|
),
|
|
953
1199
|
{ concurrency: "unbounded" }
|
|
954
1200
|
),
|
|
955
|
-
|
|
1201
|
+
Effect6.map((results) => ({
|
|
956
1202
|
written: results.filter((r) => r.success).map((r) => r.skill),
|
|
957
1203
|
errors: results.filter(
|
|
958
1204
|
(r) => !r.success
|
|
959
1205
|
).map((r) => ({ skill: r.skill, error: r.error }))
|
|
960
1206
|
}))
|
|
961
1207
|
);
|
|
962
|
-
var writeSkillsAsync = (basePath, skills, agentId) =>
|
|
1208
|
+
var writeSkillsAsync = (basePath, skills, agentId) => Effect6.runPromise(writeSkills(basePath, skills, agentId));
|
|
963
1209
|
|
|
964
1210
|
// src/lib/tui.ts
|
|
965
1211
|
init_esm_shims();
|
|
@@ -1038,8 +1284,8 @@ function resolveInstallConfig(options, config) {
|
|
|
1038
1284
|
return {
|
|
1039
1285
|
profile: options.profile ?? config.profile,
|
|
1040
1286
|
serverUrl: options.server ?? config.serverUrl,
|
|
1041
|
-
|
|
1042
|
-
|
|
1287
|
+
includeUserGlobal: options.includeUserGlobals ?? config.includeUserGlobal,
|
|
1288
|
+
includeOrgGlobal: options.includeOrgGlobals ?? config.includeOrgGlobal,
|
|
1043
1289
|
orgProjects: orgProjectsFromFlag ?? config.orgProjects,
|
|
1044
1290
|
personalProjects: personalProjectsFromFlag ?? config.personalProjects
|
|
1045
1291
|
};
|
|
@@ -1057,9 +1303,9 @@ function validateInstallOptions(resolved) {
|
|
|
1057
1303
|
log2.info(" - Use --profile, --org-projects, or --personal-projects flags");
|
|
1058
1304
|
log2.info("");
|
|
1059
1305
|
log2.info("Examples:");
|
|
1060
|
-
log2.info(" braid
|
|
1061
|
-
log2.info(" braid
|
|
1062
|
-
log2.info(" braid
|
|
1306
|
+
log2.info(" braid install --profile coding-standards");
|
|
1307
|
+
log2.info(" braid install --org-projects proj123,proj456");
|
|
1308
|
+
log2.info(" braid install --personal-projects myproj1");
|
|
1063
1309
|
process.exit(1);
|
|
1064
1310
|
}
|
|
1065
1311
|
}
|
|
@@ -1079,8 +1325,8 @@ function buildSourceDescription(resolved) {
|
|
|
1079
1325
|
function buildFetchOptions(resolved) {
|
|
1080
1326
|
const fetchOptions = {
|
|
1081
1327
|
serverUrl: resolved.serverUrl,
|
|
1082
|
-
|
|
1083
|
-
|
|
1328
|
+
includeUserGlobal: resolved.includeUserGlobal,
|
|
1329
|
+
includeOrgGlobal: resolved.includeOrgGlobal
|
|
1084
1330
|
};
|
|
1085
1331
|
if (resolved.profile) {
|
|
1086
1332
|
fetchOptions.profile = resolved.profile;
|
|
@@ -1125,15 +1371,25 @@ async function resolveAgents(options, installSpinner) {
|
|
|
1125
1371
|
return selectedAgents;
|
|
1126
1372
|
}
|
|
1127
1373
|
installSpinner.start("Detecting installed agents...");
|
|
1128
|
-
const
|
|
1129
|
-
|
|
1374
|
+
const allDetectedAgents = await detectAgentsAsync();
|
|
1375
|
+
const detectedAgents = allDetectedAgents.filter(
|
|
1376
|
+
(agent) => options.global ? agent.hasGlobalConfig : agent.hasProjectConfig
|
|
1377
|
+
);
|
|
1378
|
+
const targetType = options.global ? "global" : "project";
|
|
1379
|
+
installSpinner.stop(
|
|
1380
|
+
`Detected ${detectedAgents.length} agent(s) with ${targetType} config`
|
|
1381
|
+
);
|
|
1382
|
+
for (const agent of detectedAgents) {
|
|
1383
|
+
const targetPath = options.global ? agent.globalPath : agent.projectPath;
|
|
1384
|
+
log2.info(` ${agent.name} \u2192 ${targetPath}`);
|
|
1385
|
+
}
|
|
1130
1386
|
if (detectedAgents.length === 0) {
|
|
1131
1387
|
log2.warn("No AI coding agents detected.");
|
|
1132
1388
|
log2.info(
|
|
1133
1389
|
"Supported agents: claude-code, opencode, cursor, windsurf, cline, and more."
|
|
1134
1390
|
);
|
|
1135
1391
|
log2.info(
|
|
1136
|
-
"Use --agents to specify agents manually: braid
|
|
1392
|
+
"Use --agents to specify agents manually: braid install -p my-profile -a claude-code"
|
|
1137
1393
|
);
|
|
1138
1394
|
process.exit(1);
|
|
1139
1395
|
}
|
|
@@ -1160,50 +1416,95 @@ async function selectAgents(detectedAgents, options) {
|
|
|
1160
1416
|
}
|
|
1161
1417
|
return selected;
|
|
1162
1418
|
}
|
|
1419
|
+
async function installSkillsToAgent(agent, response, installPath) {
|
|
1420
|
+
if (response.skills.length === 0 || !agent.projectPath) {
|
|
1421
|
+
return { written: 0, errors: 0 };
|
|
1422
|
+
}
|
|
1423
|
+
const result = await writeSkillsAsync(installPath, response.skills, agent.id);
|
|
1424
|
+
for (const err of result.errors) {
|
|
1425
|
+
log2.warn(` Failed skill: ${err.skill} - ${err.error}`);
|
|
1426
|
+
}
|
|
1427
|
+
return { written: result.written.length, errors: result.errors.length };
|
|
1428
|
+
}
|
|
1429
|
+
async function installRulesToAgent(agent, response, options) {
|
|
1430
|
+
const rules = response.rules ?? [];
|
|
1431
|
+
const rulesPath = resolveRulesInstallPath(agent, {
|
|
1432
|
+
global: options.global === true
|
|
1433
|
+
});
|
|
1434
|
+
if (rules.length === 0 || !rulesPath || !agent.ruleFormat) {
|
|
1435
|
+
return { written: 0, errors: 0 };
|
|
1436
|
+
}
|
|
1437
|
+
const result = await writeRulesForAgentAsync(agent, rules, rulesPath);
|
|
1438
|
+
for (const err of result.errors) {
|
|
1439
|
+
log2.warn(` Failed rules: ${err.agent} - ${err.error}`);
|
|
1440
|
+
}
|
|
1441
|
+
return { written: result.written, errors: result.errors.length };
|
|
1442
|
+
}
|
|
1443
|
+
function formatInstallSummary(agentName, skillsWritten, rulesWritten) {
|
|
1444
|
+
const parts = [];
|
|
1445
|
+
if (skillsWritten > 0) {
|
|
1446
|
+
parts.push(`${skillsWritten} skills`);
|
|
1447
|
+
}
|
|
1448
|
+
if (rulesWritten > 0) {
|
|
1449
|
+
parts.push(`${rulesWritten} rules`);
|
|
1450
|
+
}
|
|
1451
|
+
return `${agentName}: ${parts.join(", ")} installed`;
|
|
1452
|
+
}
|
|
1163
1453
|
async function installToAgent(agent, response, serverUrl, options, installSpinner) {
|
|
1164
1454
|
const installPath = resolveInstallPath(agent, {
|
|
1165
1455
|
global: options.global === true
|
|
1166
1456
|
});
|
|
1167
1457
|
installSpinner.start(`Installing to ${agent.name}...`);
|
|
1168
|
-
const
|
|
1169
|
-
|
|
1458
|
+
const skills = installPath ? await installSkillsToAgent(agent, response, installPath) : { written: 0, errors: 0 };
|
|
1459
|
+
const rules = await installRulesToAgent(agent, response, options);
|
|
1460
|
+
const totalWritten = skills.written + rules.written;
|
|
1461
|
+
const totalErrors = skills.errors + rules.errors;
|
|
1462
|
+
if (totalErrors > 0) {
|
|
1170
1463
|
installSpinner.stop(
|
|
1171
|
-
`${agent.name}: ${
|
|
1464
|
+
`${agent.name}: ${totalWritten} installed, ${totalErrors} failed`
|
|
1172
1465
|
);
|
|
1173
|
-
for (const err of result.errors) {
|
|
1174
|
-
log2.warn(` Failed: ${err.skill} - ${err.error}`);
|
|
1175
|
-
}
|
|
1176
1466
|
} else {
|
|
1177
1467
|
installSpinner.stop(
|
|
1178
|
-
|
|
1468
|
+
formatInstallSummary(agent.name, skills.written, rules.written)
|
|
1179
1469
|
);
|
|
1180
1470
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1471
|
+
if (response.skills.length > 0 && installPath) {
|
|
1472
|
+
await updateMetadataAsync(
|
|
1473
|
+
installPath,
|
|
1474
|
+
response.skills.map((s) => ({
|
|
1475
|
+
name: s.name,
|
|
1476
|
+
source: response.source,
|
|
1477
|
+
version: response.version,
|
|
1478
|
+
serverUrl
|
|
1479
|
+
}))
|
|
1480
|
+
);
|
|
1481
|
+
}
|
|
1482
|
+
return { written: totalWritten, errors: totalErrors };
|
|
1191
1483
|
}
|
|
1192
1484
|
async function installCommand(options) {
|
|
1193
1485
|
const config = await loadMergedConfigAsync();
|
|
1194
1486
|
const resolved = resolveInstallConfig(options, config);
|
|
1195
1487
|
validateInstallOptions(resolved);
|
|
1196
1488
|
const sourceDesc = buildSourceDescription(resolved);
|
|
1197
|
-
intro2(`Installing
|
|
1489
|
+
intro2(`Installing from ${sourceDesc}`);
|
|
1198
1490
|
const installSpinner = spinner2();
|
|
1199
|
-
installSpinner.start("Fetching
|
|
1491
|
+
installSpinner.start("Fetching from Braid...");
|
|
1200
1492
|
try {
|
|
1201
1493
|
const fetchOptions = buildFetchOptions(resolved);
|
|
1202
1494
|
const response = await fetchSkillsAsync(fetchOptions);
|
|
1203
|
-
|
|
1204
|
-
|
|
1495
|
+
const ruleCount = response.rules?.length ?? 0;
|
|
1496
|
+
const skillCount = response.skills.length;
|
|
1497
|
+
const foundParts = [];
|
|
1498
|
+
if (skillCount > 0) {
|
|
1499
|
+
foundParts.push(`${skillCount} skills`);
|
|
1500
|
+
}
|
|
1501
|
+
if (ruleCount > 0) {
|
|
1502
|
+
foundParts.push(`${ruleCount} rules`);
|
|
1503
|
+
}
|
|
1504
|
+
installSpinner.stop(`Found ${foundParts.join(", ") || "nothing"}`);
|
|
1505
|
+
if (skillCount === 0 && ruleCount === 0) {
|
|
1205
1506
|
log2.warn(
|
|
1206
|
-
"No skills found. Check that your profile/project has enabled
|
|
1507
|
+
"No skills or rules found. Check that your profile/project has enabled prompts."
|
|
1207
1508
|
);
|
|
1208
1509
|
process.exit(0);
|
|
1209
1510
|
}
|
|
@@ -1228,13 +1529,13 @@ async function installCommand(options) {
|
|
|
1228
1529
|
totalErrors += result.errors;
|
|
1229
1530
|
}
|
|
1230
1531
|
if (totalErrors > 0) {
|
|
1231
|
-
outro2(`Installed ${totalWritten}
|
|
1532
|
+
outro2(`Installed ${totalWritten} items with ${totalErrors} errors.`);
|
|
1232
1533
|
} else {
|
|
1233
1534
|
outro2(
|
|
1234
|
-
`Successfully installed ${totalWritten}
|
|
1535
|
+
`Successfully installed ${totalWritten} items to ${selectedAgents.length} agent(s).`
|
|
1235
1536
|
);
|
|
1236
1537
|
}
|
|
1237
|
-
log2.info("Run 'braid
|
|
1538
|
+
log2.info("Run 'braid list' to see installed skills.");
|
|
1238
1539
|
} catch (error) {
|
|
1239
1540
|
installSpinner.stop("Install failed");
|
|
1240
1541
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1307,6 +1608,9 @@ async function listCommand(options) {
|
|
|
1307
1608
|
const installPath = resolveInstallPath(agent, {
|
|
1308
1609
|
global: options.global === true
|
|
1309
1610
|
});
|
|
1611
|
+
if (!installPath) {
|
|
1612
|
+
continue;
|
|
1613
|
+
}
|
|
1310
1614
|
const exists = await directoryExistsAsync(installPath);
|
|
1311
1615
|
if (!exists) {
|
|
1312
1616
|
continue;
|
|
@@ -1320,10 +1624,8 @@ async function listCommand(options) {
|
|
|
1320
1624
|
displayAgentSkills(agent, installPath, braidSkills);
|
|
1321
1625
|
}
|
|
1322
1626
|
if (totalSkills === 0) {
|
|
1323
|
-
log3.warn("\nNo skills installed via braid
|
|
1324
|
-
log3.info(
|
|
1325
|
-
"Run 'braid-skills install --profile <name>' to install skills."
|
|
1326
|
-
);
|
|
1627
|
+
log3.warn("\nNo skills installed via braid.");
|
|
1628
|
+
log3.info("Run 'braid install --profile <name>' to install skills.");
|
|
1327
1629
|
} else {
|
|
1328
1630
|
log3.info("");
|
|
1329
1631
|
log3.info(`Total: ${totalSkills} skill(s) installed`);
|
|
@@ -1339,7 +1641,7 @@ async function listCommand(options) {
|
|
|
1339
1641
|
// src/commands/remove.ts
|
|
1340
1642
|
init_esm_shims();
|
|
1341
1643
|
import { rm } from "fs/promises";
|
|
1342
|
-
import { join as
|
|
1644
|
+
import { join as join4, resolve as resolve3 } from "path";
|
|
1343
1645
|
import process6 from "process";
|
|
1344
1646
|
async function collectInstalledSkills(detectedAgents, options) {
|
|
1345
1647
|
const skillsToRemove = [];
|
|
@@ -1347,6 +1649,9 @@ async function collectInstalledSkills(detectedAgents, options) {
|
|
|
1347
1649
|
const installPath = resolveInstallPath(agent, {
|
|
1348
1650
|
global: options.global === true
|
|
1349
1651
|
});
|
|
1652
|
+
if (!installPath) {
|
|
1653
|
+
continue;
|
|
1654
|
+
}
|
|
1350
1655
|
const exists = await directoryExistsAsync(installPath);
|
|
1351
1656
|
if (!exists) {
|
|
1352
1657
|
continue;
|
|
@@ -1357,7 +1662,7 @@ async function collectInstalledSkills(detectedAgents, options) {
|
|
|
1357
1662
|
name: skill.name,
|
|
1358
1663
|
agentName: agent.name,
|
|
1359
1664
|
installPath,
|
|
1360
|
-
skillPath:
|
|
1665
|
+
skillPath: join4(installPath, skill.name)
|
|
1361
1666
|
});
|
|
1362
1667
|
}
|
|
1363
1668
|
}
|
|
@@ -1368,7 +1673,7 @@ async function selectSkillsToRemove(skillsToRemove, options) {
|
|
|
1368
1673
|
const selected = skillsToRemove.filter((s) => s.name === options.skill);
|
|
1369
1674
|
if (selected.length === 0) {
|
|
1370
1675
|
log2.error(`Skill '${options.skill}' not found.`);
|
|
1371
|
-
log2.info("Run 'braid
|
|
1676
|
+
log2.info("Run 'braid list' to see installed skills.");
|
|
1372
1677
|
process6.exit(1);
|
|
1373
1678
|
}
|
|
1374
1679
|
return selected;
|
|
@@ -1408,7 +1713,14 @@ async function confirmRemoval(selectedCount, options) {
|
|
|
1408
1713
|
async function removeSkill(skill, removeSpinner) {
|
|
1409
1714
|
removeSpinner.start(`Removing ${skill.name} from ${skill.agentName}...`);
|
|
1410
1715
|
try {
|
|
1411
|
-
|
|
1716
|
+
const resolvedSkillPath = resolve3(skill.skillPath);
|
|
1717
|
+
const resolvedInstallPath = resolve3(skill.installPath);
|
|
1718
|
+
if (!resolvedSkillPath.startsWith(`${resolvedInstallPath}/`)) {
|
|
1719
|
+
removeSpinner.stop(`Unsafe path for ${skill.name}`);
|
|
1720
|
+
log2.warn(" Skill path escapes install directory, skipping.");
|
|
1721
|
+
return false;
|
|
1722
|
+
}
|
|
1723
|
+
await rm(resolvedSkillPath, { recursive: true, force: true });
|
|
1412
1724
|
await removeFromMetadataAsync(skill.installPath, skill.name);
|
|
1413
1725
|
removeSpinner.stop(`Removed ${skill.name} from ${skill.agentName}`);
|
|
1414
1726
|
return true;
|
|
@@ -1435,7 +1747,7 @@ async function removeCommand(options) {
|
|
|
1435
1747
|
);
|
|
1436
1748
|
removeSpinner.stop(`Found ${skillsToRemove.length} installed skill(s)`);
|
|
1437
1749
|
if (skillsToRemove.length === 0) {
|
|
1438
|
-
log2.warn("No skills installed via braid
|
|
1750
|
+
log2.warn("No skills installed via braid.");
|
|
1439
1751
|
return;
|
|
1440
1752
|
}
|
|
1441
1753
|
const selected = await selectSkillsToRemove(skillsToRemove, options);
|
|
@@ -1473,18 +1785,25 @@ import {
|
|
|
1473
1785
|
outro as outro3,
|
|
1474
1786
|
spinner as spinner4
|
|
1475
1787
|
} from "@clack/prompts";
|
|
1476
|
-
import { Data as
|
|
1477
|
-
var UpdateError = class extends
|
|
1788
|
+
import { Data as Data6, Effect as Effect7, pipe as pipe7 } from "effect";
|
|
1789
|
+
var UpdateError = class extends Data6.TaggedError("UpdateError") {
|
|
1478
1790
|
};
|
|
1479
|
-
var UserCancelledError = class extends
|
|
1791
|
+
var UserCancelledError = class extends Data6.TaggedError("UserCancelledError") {
|
|
1480
1792
|
};
|
|
1481
|
-
|
|
1793
|
+
async function resolveValidInstallPath(agent, options) {
|
|
1794
|
+
const installPath = resolveInstallPath(agent, {
|
|
1795
|
+
global: options.global === true
|
|
1796
|
+
});
|
|
1797
|
+
if (!installPath) {
|
|
1798
|
+
return null;
|
|
1799
|
+
}
|
|
1800
|
+
const exists = await directoryExistsAsync(installPath);
|
|
1801
|
+
return exists ? installPath : null;
|
|
1802
|
+
}
|
|
1803
|
+
var collectSourcesFromAgent = (agent, options, sourcesToUpdate) => Effect7.tryPromise({
|
|
1482
1804
|
try: async () => {
|
|
1483
|
-
const installPath =
|
|
1484
|
-
|
|
1485
|
-
});
|
|
1486
|
-
const exists = await directoryExistsAsync(installPath);
|
|
1487
|
-
if (!exists) {
|
|
1805
|
+
const installPath = await resolveValidInstallPath(agent, options);
|
|
1806
|
+
if (!installPath) {
|
|
1488
1807
|
return;
|
|
1489
1808
|
}
|
|
1490
1809
|
const metadata = await readMetadataAsync(installPath);
|
|
@@ -1512,10 +1831,10 @@ var collectSourcesFromAgent = (agent, options, sourcesToUpdate) => Effect6.tryPr
|
|
|
1512
1831
|
},
|
|
1513
1832
|
catch: () => new UpdateError({ message: "Failed to collect sources" })
|
|
1514
1833
|
});
|
|
1515
|
-
var collectSources = (detectedAgents, options) =>
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
(sourcesToUpdate) =>
|
|
1834
|
+
var collectSources = (detectedAgents, options) => pipe7(
|
|
1835
|
+
Effect7.succeed(/* @__PURE__ */ new Map()),
|
|
1836
|
+
Effect7.tap(
|
|
1837
|
+
(sourcesToUpdate) => Effect7.forEach(
|
|
1519
1838
|
detectedAgents,
|
|
1520
1839
|
(agent) => collectSourcesFromAgent(agent, options, sourcesToUpdate),
|
|
1521
1840
|
{ concurrency: 1 }
|
|
@@ -1524,9 +1843,9 @@ var collectSources = (detectedAgents, options) => pipe6(
|
|
|
1524
1843
|
);
|
|
1525
1844
|
var selectSources = (sourcesToUpdate, options) => {
|
|
1526
1845
|
if (options.yes) {
|
|
1527
|
-
return
|
|
1846
|
+
return Effect7.succeed(sourcesToUpdate);
|
|
1528
1847
|
}
|
|
1529
|
-
return
|
|
1848
|
+
return Effect7.tryPromise({
|
|
1530
1849
|
try: async () => {
|
|
1531
1850
|
const sources = Array.from(sourcesToUpdate.entries()).map(
|
|
1532
1851
|
([key, source]) => ({
|
|
@@ -1577,7 +1896,7 @@ var buildFetchOptionsForSource = (source, options) => {
|
|
|
1577
1896
|
}
|
|
1578
1897
|
return fetchOptions;
|
|
1579
1898
|
};
|
|
1580
|
-
var updateAgentSkills = (agentId, agentName, installPath, response, serverUrl, updateSpinner) =>
|
|
1899
|
+
var updateAgentSkills = (agentId, agentName, installPath, response, serverUrl, updateSpinner) => Effect7.tryPromise({
|
|
1581
1900
|
try: async () => {
|
|
1582
1901
|
updateSpinner.start(`Updating ${agentName}...`);
|
|
1583
1902
|
const result = await writeSkillsAsync(
|
|
@@ -1615,15 +1934,15 @@ var updateSource = (source, options, updateSpinner) => {
|
|
|
1615
1934
|
const serverUrl = options.server ?? source.serverUrl;
|
|
1616
1935
|
const fetchOptions = buildFetchOptionsForSource(source, options);
|
|
1617
1936
|
if (fetchOptions === null) {
|
|
1618
|
-
return
|
|
1937
|
+
return Effect7.fail(
|
|
1619
1938
|
new UpdateError({
|
|
1620
|
-
message: "Skills installed with legacy metadata format. Please reinstall using 'braid
|
|
1939
|
+
message: "Skills installed with legacy metadata format. Please reinstall using 'braid install --profile <name>' or 'braid install --projects <names>'.",
|
|
1621
1940
|
source: sourceDesc
|
|
1622
1941
|
})
|
|
1623
1942
|
);
|
|
1624
1943
|
}
|
|
1625
|
-
return
|
|
1626
|
-
|
|
1944
|
+
return pipe7(
|
|
1945
|
+
Effect7.tryPromise({
|
|
1627
1946
|
try: async () => {
|
|
1628
1947
|
updateSpinner.start(`Fetching latest skills from ${sourceDesc}...`);
|
|
1629
1948
|
const response = await fetchSkillsAsync(fetchOptions);
|
|
@@ -1637,9 +1956,9 @@ var updateSource = (source, options, updateSpinner) => {
|
|
|
1637
1956
|
source: sourceDesc
|
|
1638
1957
|
})
|
|
1639
1958
|
}),
|
|
1640
|
-
|
|
1641
|
-
(response) =>
|
|
1642
|
-
|
|
1959
|
+
Effect7.flatMap(
|
|
1960
|
+
(response) => pipe7(
|
|
1961
|
+
Effect7.forEach(
|
|
1643
1962
|
source.agents,
|
|
1644
1963
|
({ agentId, agentName, installPath }) => updateAgentSkills(
|
|
1645
1964
|
agentId,
|
|
@@ -1651,7 +1970,7 @@ var updateSource = (source, options, updateSpinner) => {
|
|
|
1651
1970
|
),
|
|
1652
1971
|
{ concurrency: 1 }
|
|
1653
1972
|
),
|
|
1654
|
-
|
|
1973
|
+
Effect7.map((results) => ({
|
|
1655
1974
|
updated: results.reduce((sum, r) => sum + r.updated, 0),
|
|
1656
1975
|
errors: results.reduce((sum, r) => sum + r.errors, 0)
|
|
1657
1976
|
}))
|
|
@@ -1659,22 +1978,22 @@ var updateSource = (source, options, updateSpinner) => {
|
|
|
1659
1978
|
)
|
|
1660
1979
|
);
|
|
1661
1980
|
};
|
|
1662
|
-
var updateAllSources = (sources, options, updateSpinner) =>
|
|
1663
|
-
|
|
1981
|
+
var updateAllSources = (sources, options, updateSpinner) => pipe7(
|
|
1982
|
+
Effect7.forEach(
|
|
1664
1983
|
Array.from(sources.values()),
|
|
1665
|
-
(source) =>
|
|
1984
|
+
(source) => pipe7(
|
|
1666
1985
|
updateSource(source, options, updateSpinner),
|
|
1667
|
-
|
|
1986
|
+
Effect7.catchAll((error) => {
|
|
1668
1987
|
updateSpinner.stop(
|
|
1669
1988
|
`Failed to update from ${getSourceDesc(source)}`
|
|
1670
1989
|
);
|
|
1671
1990
|
log4.error(` ${error.message}`);
|
|
1672
|
-
return
|
|
1991
|
+
return Effect7.succeed({ updated: 0, errors: 1 });
|
|
1673
1992
|
})
|
|
1674
1993
|
),
|
|
1675
1994
|
{ concurrency: 1 }
|
|
1676
1995
|
),
|
|
1677
|
-
|
|
1996
|
+
Effect7.map((results) => ({
|
|
1678
1997
|
totalUpdated: results.reduce((sum, r) => sum + r.updated, 0),
|
|
1679
1998
|
totalErrors: results.reduce((sum, r) => sum + r.errors, 0)
|
|
1680
1999
|
}))
|
|
@@ -1685,11 +2004,9 @@ var handleUpdateError = (error, updateSpinner) => {
|
|
|
1685
2004
|
log4.warn(error.message);
|
|
1686
2005
|
return;
|
|
1687
2006
|
}
|
|
1688
|
-
if (error.message === "No skills installed via braid
|
|
2007
|
+
if (error.message === "No skills installed via braid.") {
|
|
1689
2008
|
log4.warn(error.message);
|
|
1690
|
-
log4.info(
|
|
1691
|
-
"Run 'braid-skills install --profile <name>' to install skills first."
|
|
1692
|
-
);
|
|
2009
|
+
log4.info("Run 'braid install --profile <name>' to install skills first.");
|
|
1693
2010
|
return;
|
|
1694
2011
|
}
|
|
1695
2012
|
log4.error(error.message);
|
|
@@ -1715,28 +2032,28 @@ var handleProgramExit = (result, updateSpinner) => {
|
|
|
1715
2032
|
async function updateCommand(options) {
|
|
1716
2033
|
const updateSpinner = spinner4();
|
|
1717
2034
|
updateSpinner.start("Scanning for installed skills...");
|
|
1718
|
-
const program2 =
|
|
1719
|
-
|
|
2035
|
+
const program2 = pipe7(
|
|
2036
|
+
Effect7.tryPromise({
|
|
1720
2037
|
try: () => detectAgentsAsync(),
|
|
1721
2038
|
catch: () => new UpdateError({ message: "Failed to detect agents" })
|
|
1722
2039
|
}),
|
|
1723
|
-
|
|
2040
|
+
Effect7.filterOrFail(
|
|
1724
2041
|
(agents) => agents.length > 0,
|
|
1725
2042
|
() => new UpdateError({ message: "No AI coding agents detected." })
|
|
1726
2043
|
),
|
|
1727
|
-
|
|
1728
|
-
|
|
2044
|
+
Effect7.flatMap((detectedAgents) => collectSources(detectedAgents, options)),
|
|
2045
|
+
Effect7.tap((sources) => {
|
|
1729
2046
|
updateSpinner.stop(`Found ${sources.size} source(s) to update`);
|
|
1730
2047
|
}),
|
|
1731
|
-
|
|
2048
|
+
Effect7.filterOrFail(
|
|
1732
2049
|
(sources) => sources.size > 0,
|
|
1733
|
-
() => new UpdateError({ message: "No skills installed via braid
|
|
2050
|
+
() => new UpdateError({ message: "No skills installed via braid." })
|
|
1734
2051
|
),
|
|
1735
|
-
|
|
1736
|
-
|
|
2052
|
+
Effect7.flatMap((sources) => selectSources(sources, options)),
|
|
2053
|
+
Effect7.flatMap(
|
|
1737
2054
|
(selectedSources) => updateAllSources(selectedSources, options, updateSpinner)
|
|
1738
2055
|
),
|
|
1739
|
-
|
|
2056
|
+
Effect7.tap(({ totalUpdated, totalErrors }) => {
|
|
1740
2057
|
if (totalErrors > 0) {
|
|
1741
2058
|
outro3(`Updated ${totalUpdated} skills with ${totalErrors} errors.`);
|
|
1742
2059
|
} else {
|
|
@@ -1744,14 +2061,14 @@ async function updateCommand(options) {
|
|
|
1744
2061
|
}
|
|
1745
2062
|
})
|
|
1746
2063
|
);
|
|
1747
|
-
const result = await
|
|
2064
|
+
const result = await Effect7.runPromiseExit(program2);
|
|
1748
2065
|
handleProgramExit(result, updateSpinner);
|
|
1749
2066
|
}
|
|
1750
2067
|
|
|
1751
2068
|
// src/index.ts
|
|
1752
2069
|
var program = new Command();
|
|
1753
|
-
program.name("braid
|
|
1754
|
-
"Install Braid
|
|
2070
|
+
program.name("braid").description(
|
|
2071
|
+
"Install Braid prompts as agent skills to your local development environment"
|
|
1755
2072
|
).version("0.1.0");
|
|
1756
2073
|
var auth = program.command("auth").description("Configure API key for Braid authentication");
|
|
1757
2074
|
auth.command("login", { isDefault: true }).description("Configure API key").option("-s, --server <url>", "Braid server URL (for review apps, local dev)").action(authCommand);
|
|
@@ -1765,11 +2082,11 @@ program.command("install").alias("add").description("Install skills from a profi
|
|
|
1765
2082
|
"Comma-separated personal project IDs to install from"
|
|
1766
2083
|
).option(
|
|
1767
2084
|
"--include-user-globals",
|
|
1768
|
-
"Include user's personal global
|
|
1769
|
-
).option("--no-include-user-globals", "Exclude user's personal global
|
|
2085
|
+
"Include user's personal global prompts (default: true)"
|
|
2086
|
+
).option("--no-include-user-globals", "Exclude user's personal global prompts").option(
|
|
1770
2087
|
"--include-org-globals",
|
|
1771
|
-
"Include organization's global
|
|
1772
|
-
).option("--no-include-org-globals", "Exclude organization's global
|
|
2088
|
+
"Include organization's global prompts (default: true)"
|
|
2089
|
+
).option("--no-include-org-globals", "Exclude organization's global prompts").option(
|
|
1773
2090
|
"-a, --agents <list>",
|
|
1774
2091
|
"Comma-separated list of agents (e.g., claude-code,opencode)"
|
|
1775
2092
|
).option("-g, --global", "Install to global agent directories").option("-y, --yes", "Skip confirmation prompts").option("-l, --list", "Preview skills without installing").option("-s, --server <url>", "Braid server URL (for review apps, local dev)").action(installCommand);
|