facult 2.6.0 → 2.7.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/README.md +145 -337
- package/package.json +1 -1
- package/src/adapters/codex.ts +1 -1
- package/src/audit/agent.ts +26 -24
- package/src/audit/fix.ts +875 -0
- package/src/audit/index.ts +51 -2
- package/src/audit/safe.ts +596 -0
- package/src/audit/static.ts +151 -34
- package/src/audit/status.ts +21 -0
- package/src/audit/suppressions.ts +266 -0
- package/src/audit/tui.ts +784 -174
- package/src/audit/update-index.ts +4 -17
- package/src/builtin.ts +7 -1
- package/src/cli-ui.ts +375 -0
- package/src/consolidate.ts +151 -55
- package/src/doctor.ts +327 -0
- package/src/global-docs.ts +43 -2
- package/src/index.ts +571 -292
- package/src/manage.ts +931 -88
- package/src/mcp-config.ts +132 -0
- package/src/project-sync.ts +288 -0
- package/src/remote.ts +387 -117
- package/src/trust.ts +119 -11
- package/src/util/git.ts +95 -0
package/src/manage.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import {
|
|
2
3
|
cp,
|
|
3
4
|
lstat,
|
|
@@ -15,6 +16,7 @@ import { renderCanonicalText } from "./agents";
|
|
|
15
16
|
import { ensureAiIndexPath } from "./ai-state";
|
|
16
17
|
import { builtinSyncDefaultsEnabled, facultBuiltinPackRoot } from "./builtin";
|
|
17
18
|
import { parseCliContextArgs, resolveCliContextRoot } from "./cli-context";
|
|
19
|
+
import { renderBullets, renderCode, renderPage } from "./cli-ui";
|
|
18
20
|
import { contentHash, normalizeText } from "./conflicts";
|
|
19
21
|
import {
|
|
20
22
|
globalDocTargetPaths,
|
|
@@ -31,12 +33,18 @@ import {
|
|
|
31
33
|
type FacultIndex,
|
|
32
34
|
type SkillEntry,
|
|
33
35
|
} from "./index-builder";
|
|
36
|
+
import {
|
|
37
|
+
extractServersObject,
|
|
38
|
+
loadCanonicalMcpState,
|
|
39
|
+
stringifyCanonicalMcpServers,
|
|
40
|
+
} from "./mcp-config";
|
|
34
41
|
import {
|
|
35
42
|
facultMachineStateDir,
|
|
36
43
|
facultRootDir,
|
|
37
44
|
legacyFacultStateDirForRoot,
|
|
38
45
|
projectRootFromAiRoot,
|
|
39
46
|
} from "./paths";
|
|
47
|
+
import { loadProjectToolSyncPolicy } from "./project-sync";
|
|
40
48
|
|
|
41
49
|
export interface ManagedToolState {
|
|
42
50
|
tool: string;
|
|
@@ -44,6 +52,8 @@ export interface ManagedToolState {
|
|
|
44
52
|
skillsDir?: string;
|
|
45
53
|
mcpConfig?: string;
|
|
46
54
|
agentsDir?: string;
|
|
55
|
+
pluginsDir?: string;
|
|
56
|
+
pluginMarketplacePath?: string;
|
|
47
57
|
automationDir?: string;
|
|
48
58
|
toolHome?: string;
|
|
49
59
|
globalAgentsPath?: string;
|
|
@@ -53,6 +63,8 @@ export interface ManagedToolState {
|
|
|
53
63
|
skillsBackup?: string | null;
|
|
54
64
|
mcpBackup?: string | null;
|
|
55
65
|
agentsBackup?: string | null;
|
|
66
|
+
pluginsBackup?: string | null;
|
|
67
|
+
pluginMarketplaceBackup?: string | null;
|
|
56
68
|
globalAgentsBackup?: string | null;
|
|
57
69
|
globalAgentsOverrideBackup?: string | null;
|
|
58
70
|
rulesBackup?: string | null;
|
|
@@ -76,6 +88,8 @@ export interface ToolPaths {
|
|
|
76
88
|
skillsDir?: string;
|
|
77
89
|
mcpConfig?: string;
|
|
78
90
|
agentsDir?: string;
|
|
91
|
+
pluginsDir?: string;
|
|
92
|
+
pluginMarketplacePath?: string;
|
|
79
93
|
automationDir?: string;
|
|
80
94
|
toolHome?: string;
|
|
81
95
|
rulesDir?: string;
|
|
@@ -111,6 +125,44 @@ function homePath(home: string, ...parts: string[]): string {
|
|
|
111
125
|
return join(home, ...parts);
|
|
112
126
|
}
|
|
113
127
|
|
|
128
|
+
function codexLiveRoot(home: string, rootDir?: string): string {
|
|
129
|
+
const projectRoot = rootDir ? projectRootFromAiRoot(rootDir, home) : null;
|
|
130
|
+
return projectRoot ?? home;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function codexPluginsDir(home: string, rootDir?: string): string {
|
|
134
|
+
return join(codexLiveRoot(home, rootDir), "plugins");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function codexSkillsDir(home: string, rootDir?: string): string {
|
|
138
|
+
return join(codexLiveRoot(home, rootDir), ".agents", "skills");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function codexPluginMarketplacePath(home: string, rootDir?: string): string {
|
|
142
|
+
return join(
|
|
143
|
+
codexLiveRoot(home, rootDir),
|
|
144
|
+
".agents",
|
|
145
|
+
"plugins",
|
|
146
|
+
"marketplace.json"
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function codexLegacySkillsDir(home: string, rootDir?: string): string {
|
|
151
|
+
return join(codexLiveRoot(home, rootDir), ".codex", "skills");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function codexLegacyPluginsDir(home: string, rootDir?: string): string {
|
|
155
|
+
return join(codexLiveRoot(home, rootDir), ".codex", "plugins");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function codexCanonicalPluginsRoot(rootDir: string): string {
|
|
159
|
+
return join(rootDir, "tools", "codex", "plugins");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function codexCanonicalPluginMarketplacePath(rootDir: string): string {
|
|
163
|
+
return join(codexCanonicalPluginsRoot(rootDir), "marketplace.json");
|
|
164
|
+
}
|
|
165
|
+
|
|
114
166
|
function expandHomePath(pathValue: string, home: string): string {
|
|
115
167
|
if (pathValue === "~") {
|
|
116
168
|
return home;
|
|
@@ -146,6 +198,79 @@ function renderedHash(text: string): string {
|
|
|
146
198
|
return contentHash(normalizeText(text));
|
|
147
199
|
}
|
|
148
200
|
|
|
201
|
+
type ManagedTargetContent = string | Uint8Array;
|
|
202
|
+
|
|
203
|
+
function byteHash(content: Uint8Array): string {
|
|
204
|
+
return createHash("sha256").update(content).digest("hex");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function targetContentHash(
|
|
208
|
+
content: ManagedTargetContent,
|
|
209
|
+
options?: { normalizeText?: boolean }
|
|
210
|
+
): string {
|
|
211
|
+
if (typeof content === "string") {
|
|
212
|
+
return options?.normalizeText === false
|
|
213
|
+
? byteHash(Buffer.from(content))
|
|
214
|
+
: renderedHash(content);
|
|
215
|
+
}
|
|
216
|
+
return byteHash(content);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function readTargetHash(
|
|
220
|
+
pathValue: string,
|
|
221
|
+
options?: { normalizeText?: boolean }
|
|
222
|
+
): Promise<string | null> {
|
|
223
|
+
if (!(await fileExists(pathValue))) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
if (options?.normalizeText === false) {
|
|
227
|
+
return byteHash(await Bun.file(pathValue).bytes());
|
|
228
|
+
}
|
|
229
|
+
return renderedHash(await Bun.file(pathValue).text());
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function normalizeCodexMarketplaceText(text: string): string {
|
|
233
|
+
try {
|
|
234
|
+
const parsed = JSON.parse(text) as unknown;
|
|
235
|
+
if (!isPlainObject(parsed)) {
|
|
236
|
+
return text.endsWith("\n") ? text : `${text}\n`;
|
|
237
|
+
}
|
|
238
|
+
const plugins = Array.isArray(parsed.plugins) ? parsed.plugins : null;
|
|
239
|
+
if (plugins) {
|
|
240
|
+
parsed.plugins = plugins.map((entry) => {
|
|
241
|
+
if (!isPlainObject(entry)) {
|
|
242
|
+
return entry;
|
|
243
|
+
}
|
|
244
|
+
const source = isPlainObject(entry.source) ? { ...entry.source } : null;
|
|
245
|
+
if (
|
|
246
|
+
source?.source === "local" &&
|
|
247
|
+
typeof source.path === "string" &&
|
|
248
|
+
source.path.startsWith("./.codex/plugins/")
|
|
249
|
+
) {
|
|
250
|
+
source.path = source.path.replace("./.codex/plugins/", "./plugins/");
|
|
251
|
+
}
|
|
252
|
+
return source ? { ...entry, source } : entry;
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return `${JSON.stringify(parsed, null, 2)}\n`;
|
|
256
|
+
} catch {
|
|
257
|
+
return text.endsWith("\n") ? text : `${text}\n`;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function isSafeCodexPluginName(name: string): boolean {
|
|
262
|
+
const trimmed = name.trim();
|
|
263
|
+
return (
|
|
264
|
+
trimmed.length > 0 &&
|
|
265
|
+
trimmed !== "." &&
|
|
266
|
+
trimmed !== ".." &&
|
|
267
|
+
!trimmed.includes("..") &&
|
|
268
|
+
!trimmed.includes("/") &&
|
|
269
|
+
!trimmed.includes("\\") &&
|
|
270
|
+
basename(trimmed) === trimmed
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
149
274
|
function defaultToolPaths(
|
|
150
275
|
home: string,
|
|
151
276
|
rootDir?: string
|
|
@@ -162,9 +287,13 @@ function defaultToolPaths(
|
|
|
162
287
|
},
|
|
163
288
|
codex: {
|
|
164
289
|
tool: "codex",
|
|
165
|
-
skillsDir:
|
|
290
|
+
skillsDir: codexSkillsDir(home, rootDir),
|
|
166
291
|
mcpConfig: toolBase(".codex", "mcp.json"),
|
|
167
292
|
agentsDir: toolBase(".codex", "agents"),
|
|
293
|
+
pluginsDir: projectRoot ? undefined : codexPluginsDir(home, rootDir),
|
|
294
|
+
pluginMarketplacePath: projectRoot
|
|
295
|
+
? undefined
|
|
296
|
+
: codexPluginMarketplacePath(home, rootDir),
|
|
168
297
|
automationDir: homePath(home, ".codex", "automations"),
|
|
169
298
|
toolHome: toolBase(".codex"),
|
|
170
299
|
rulesDir: toolBase(".codex", "rules"),
|
|
@@ -264,6 +393,7 @@ async function resolveToolPaths(
|
|
|
264
393
|
override?: Record<string, ToolPaths>
|
|
265
394
|
): Promise<ToolPaths | null> {
|
|
266
395
|
const defaults = defaultToolPaths(home, rootDir);
|
|
396
|
+
const projectRoot = rootDir ? projectRootFromAiRoot(rootDir, home) : null;
|
|
267
397
|
if (override?.[tool]) {
|
|
268
398
|
const base = defaults[tool] ?? null;
|
|
269
399
|
return base ? { ...base, ...override[tool] } : (override[tool] ?? null);
|
|
@@ -280,6 +410,10 @@ async function resolveToolPaths(
|
|
|
280
410
|
return base;
|
|
281
411
|
}
|
|
282
412
|
|
|
413
|
+
if (projectRoot) {
|
|
414
|
+
return base;
|
|
415
|
+
}
|
|
416
|
+
|
|
283
417
|
const adapterPaths = getAdapter("codex")?.getDefaultPaths?.();
|
|
284
418
|
const adapterConfig = adapterPaths?.config
|
|
285
419
|
? expandHomePath(adapterPaths.config, home)
|
|
@@ -629,6 +763,96 @@ async function canonicalAutomationsExist(rootDir: string): Promise<boolean> {
|
|
|
629
763
|
}
|
|
630
764
|
}
|
|
631
765
|
|
|
766
|
+
interface CanonicalPluginEntry {
|
|
767
|
+
name: string;
|
|
768
|
+
sourceDir: string;
|
|
769
|
+
files: Map<string, Uint8Array>;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
async function listRelativeFilesWithDotfiles(root: string): Promise<string[]> {
|
|
773
|
+
const out: string[] = [];
|
|
774
|
+
|
|
775
|
+
async function visit(currentDir: string, prefix = ""): Promise<void> {
|
|
776
|
+
const entries = await readdir(currentDir, { withFileTypes: true }).catch(
|
|
777
|
+
() => [] as import("node:fs").Dirent[]
|
|
778
|
+
);
|
|
779
|
+
for (const entry of entries) {
|
|
780
|
+
const relPath = prefix ? join(prefix, entry.name) : entry.name;
|
|
781
|
+
const fullPath = join(currentDir, entry.name);
|
|
782
|
+
if (entry.isDirectory()) {
|
|
783
|
+
await visit(fullPath, relPath);
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
if (entry.isFile()) {
|
|
787
|
+
out.push(relPath);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
await visit(root);
|
|
793
|
+
return out.sort();
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
async function loadCanonicalCodexPlugins(
|
|
797
|
+
rootDir: string
|
|
798
|
+
): Promise<CanonicalPluginEntry[]> {
|
|
799
|
+
const pluginsRoot = codexCanonicalPluginsRoot(rootDir);
|
|
800
|
+
const entries = await readdir(pluginsRoot, { withFileTypes: true }).catch(
|
|
801
|
+
() => [] as import("node:fs").Dirent[]
|
|
802
|
+
);
|
|
803
|
+
const out: CanonicalPluginEntry[] = [];
|
|
804
|
+
|
|
805
|
+
for (const entry of entries) {
|
|
806
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
const sourceDir = join(pluginsRoot, entry.name);
|
|
810
|
+
if (!(await fileExists(join(sourceDir, ".codex-plugin", "plugin.json")))) {
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
const files = new Map<string, Uint8Array>();
|
|
814
|
+
for (const relPath of await listRelativeFilesWithDotfiles(sourceDir)) {
|
|
815
|
+
files.set(relPath, await Bun.file(join(sourceDir, relPath)).bytes());
|
|
816
|
+
}
|
|
817
|
+
out.push({ name: entry.name, sourceDir, files });
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
async function canonicalCodexPluginsExist(rootDir: string): Promise<boolean> {
|
|
824
|
+
if (await fileExists(codexCanonicalPluginMarketplacePath(rootDir))) {
|
|
825
|
+
return true;
|
|
826
|
+
}
|
|
827
|
+
return (await loadCanonicalCodexPlugins(rootDir)).length > 0;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
async function loadCanonicalCodexMarketplaceText(
|
|
831
|
+
rootDir: string
|
|
832
|
+
): Promise<{ text: string | null; sourcePath: string }> {
|
|
833
|
+
const sourcePath = codexCanonicalPluginMarketplacePath(rootDir);
|
|
834
|
+
const raw = await readTextOrNull(sourcePath);
|
|
835
|
+
return {
|
|
836
|
+
text: raw == null ? null : normalizeCodexMarketplaceText(raw),
|
|
837
|
+
sourcePath,
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
async function hashDirectoryTree(root: string): Promise<string | null> {
|
|
842
|
+
if (!(await fileExists(root))) {
|
|
843
|
+
return null;
|
|
844
|
+
}
|
|
845
|
+
const files = await listRelativeFilesWithDotfiles(root);
|
|
846
|
+
const hash = createHash("sha256");
|
|
847
|
+
for (const relPath of files) {
|
|
848
|
+
hash.update(relPath);
|
|
849
|
+
hash.update("\0");
|
|
850
|
+
hash.update(await Bun.file(join(root, relPath)).bytes());
|
|
851
|
+
hash.update("\0");
|
|
852
|
+
}
|
|
853
|
+
return hash.digest("hex");
|
|
854
|
+
}
|
|
855
|
+
|
|
632
856
|
async function loadMergedIndex(
|
|
633
857
|
homeDir: string,
|
|
634
858
|
rootDir: string
|
|
@@ -650,7 +874,11 @@ async function loadEnabledSkillEntries(args: {
|
|
|
650
874
|
tool: string;
|
|
651
875
|
}): Promise<{ name: string; path: string }[]> {
|
|
652
876
|
const index = await loadMergedIndex(args.homeDir, args.rootDir);
|
|
653
|
-
const
|
|
877
|
+
const projectPolicy = await loadProjectToolSyncPolicy(args);
|
|
878
|
+
const useBuiltinDefaults = await builtinSyncDefaultsEnabled(
|
|
879
|
+
args.rootDir,
|
|
880
|
+
args.homeDir
|
|
881
|
+
);
|
|
654
882
|
const out: { name: string; path: string }[] = [];
|
|
655
883
|
|
|
656
884
|
for (const [name, entry] of Object.entries(index.skills)) {
|
|
@@ -668,6 +896,15 @@ async function loadEnabledSkillEntries(args: {
|
|
|
668
896
|
) {
|
|
669
897
|
continue;
|
|
670
898
|
}
|
|
899
|
+
if (
|
|
900
|
+
projectPolicy &&
|
|
901
|
+
!(
|
|
902
|
+
projectPolicy.skills.includes("*") ||
|
|
903
|
+
projectPolicy.skills.includes(name)
|
|
904
|
+
)
|
|
905
|
+
) {
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
671
908
|
out.push({ name, path: skill.path });
|
|
672
909
|
}
|
|
673
910
|
|
|
@@ -675,6 +912,10 @@ async function loadEnabledSkillEntries(args: {
|
|
|
675
912
|
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
676
913
|
}
|
|
677
914
|
|
|
915
|
+
if (projectPolicy) {
|
|
916
|
+
return [];
|
|
917
|
+
}
|
|
918
|
+
|
|
678
919
|
return (await listSkillDirs(join(args.rootDir, "skills"))).map((name) => ({
|
|
679
920
|
name,
|
|
680
921
|
path: join(args.rootDir, "skills", name),
|
|
@@ -684,9 +925,14 @@ async function loadEnabledSkillEntries(args: {
|
|
|
684
925
|
async function loadManagedAgentEntries(args: {
|
|
685
926
|
homeDir: string;
|
|
686
927
|
rootDir: string;
|
|
928
|
+
tool: string;
|
|
687
929
|
}): Promise<{ name: string; sourcePath: string; raw: string }[]> {
|
|
688
930
|
const index = await loadMergedIndex(args.homeDir, args.rootDir);
|
|
689
|
-
const
|
|
931
|
+
const projectPolicy = await loadProjectToolSyncPolicy(args);
|
|
932
|
+
const useBuiltinDefaults = await builtinSyncDefaultsEnabled(
|
|
933
|
+
args.rootDir,
|
|
934
|
+
args.homeDir
|
|
935
|
+
);
|
|
690
936
|
const out: { name: string; sourcePath: string; raw: string }[] = [];
|
|
691
937
|
|
|
692
938
|
for (const [name, entry] of Object.entries(index.agents)) {
|
|
@@ -698,6 +944,15 @@ async function loadManagedAgentEntries(args: {
|
|
|
698
944
|
) {
|
|
699
945
|
continue;
|
|
700
946
|
}
|
|
947
|
+
if (
|
|
948
|
+
projectPolicy &&
|
|
949
|
+
!(
|
|
950
|
+
projectPolicy.agents.includes("*") ||
|
|
951
|
+
projectPolicy.agents.includes(name)
|
|
952
|
+
)
|
|
953
|
+
) {
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
701
956
|
const raw = await readTextIfExists(agent.path);
|
|
702
957
|
if (raw == null) {
|
|
703
958
|
continue;
|
|
@@ -709,6 +964,10 @@ async function loadManagedAgentEntries(args: {
|
|
|
709
964
|
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
710
965
|
}
|
|
711
966
|
|
|
967
|
+
if (projectPolicy) {
|
|
968
|
+
return [];
|
|
969
|
+
}
|
|
970
|
+
|
|
712
971
|
return await loadCanonicalAgents(args.rootDir);
|
|
713
972
|
}
|
|
714
973
|
|
|
@@ -728,7 +987,7 @@ async function planAgentFileChanges({
|
|
|
728
987
|
contents: Map<string, string>;
|
|
729
988
|
sources: Map<string, string>;
|
|
730
989
|
}> {
|
|
731
|
-
const agents = await loadManagedAgentEntries({ homeDir, rootDir });
|
|
990
|
+
const agents = await loadManagedAgentEntries({ homeDir, rootDir, tool });
|
|
732
991
|
const contents = new Map<string, string>();
|
|
733
992
|
const sources = new Map<string, string>();
|
|
734
993
|
const desiredPaths = new Set<string>();
|
|
@@ -890,6 +1149,10 @@ async function listSkillDirs(skillsRoot: string): Promise<string[]> {
|
|
|
890
1149
|
}
|
|
891
1150
|
}
|
|
892
1151
|
|
|
1152
|
+
async function canonicalSkillsExist(rootDir: string): Promise<boolean> {
|
|
1153
|
+
return (await listSkillDirs(join(rootDir, "skills"))).length > 0;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
893
1156
|
async function loadEnabledSkillNames({
|
|
894
1157
|
homeDir,
|
|
895
1158
|
rootDir,
|
|
@@ -903,24 +1166,6 @@ async function loadEnabledSkillNames({
|
|
|
903
1166
|
return entries.map((entry) => entry.name);
|
|
904
1167
|
}
|
|
905
1168
|
|
|
906
|
-
function extractServersObject(parsed: unknown): Record<string, unknown> | null {
|
|
907
|
-
if (!isPlainObject(parsed)) {
|
|
908
|
-
return null;
|
|
909
|
-
}
|
|
910
|
-
const raw = parsed as Record<string, unknown>;
|
|
911
|
-
const servers =
|
|
912
|
-
(raw.servers as Record<string, unknown> | undefined) ??
|
|
913
|
-
(raw.mcpServers as Record<string, unknown> | undefined) ??
|
|
914
|
-
((raw.mcp as Record<string, unknown> | undefined)?.servers as
|
|
915
|
-
| Record<string, unknown>
|
|
916
|
-
| undefined) ??
|
|
917
|
-
null;
|
|
918
|
-
if (servers && isPlainObject(servers)) {
|
|
919
|
-
return servers;
|
|
920
|
-
}
|
|
921
|
-
return null;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
1169
|
function canonicalServerToToolConfig(server: unknown): unknown {
|
|
925
1170
|
if (!isPlainObject(server)) {
|
|
926
1171
|
return server;
|
|
@@ -949,18 +1194,34 @@ function canonicalServerToToolConfig(server: unknown): unknown {
|
|
|
949
1194
|
return out;
|
|
950
1195
|
}
|
|
951
1196
|
|
|
952
|
-
function filterServersForTool(
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1197
|
+
async function filterServersForTool(args: {
|
|
1198
|
+
homeDir: string;
|
|
1199
|
+
rootDir: string;
|
|
1200
|
+
servers: Record<string, unknown>;
|
|
1201
|
+
tool: string;
|
|
1202
|
+
}): Promise<Record<string, unknown>> {
|
|
1203
|
+
const projectPolicy = await loadProjectToolSyncPolicy({
|
|
1204
|
+
homeDir: args.homeDir,
|
|
1205
|
+
rootDir: args.rootDir,
|
|
1206
|
+
tool: args.tool,
|
|
1207
|
+
});
|
|
956
1208
|
const out: Record<string, unknown> = {};
|
|
957
|
-
for (const [name, cfg] of Object.entries(servers)) {
|
|
1209
|
+
for (const [name, cfg] of Object.entries(args.servers)) {
|
|
958
1210
|
if (isPlainObject(cfg)) {
|
|
959
1211
|
const enabledFor = cfg.enabledFor;
|
|
960
|
-
if (Array.isArray(enabledFor) && !enabledFor.includes(tool)) {
|
|
1212
|
+
if (Array.isArray(enabledFor) && !enabledFor.includes(args.tool)) {
|
|
961
1213
|
continue;
|
|
962
1214
|
}
|
|
963
1215
|
}
|
|
1216
|
+
if (
|
|
1217
|
+
projectPolicy &&
|
|
1218
|
+
!(
|
|
1219
|
+
projectPolicy.mcpServers.includes("*") ||
|
|
1220
|
+
projectPolicy.mcpServers.includes(name)
|
|
1221
|
+
)
|
|
1222
|
+
) {
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
964
1225
|
out[name] = canonicalServerToToolConfig(cfg);
|
|
965
1226
|
}
|
|
966
1227
|
return out;
|
|
@@ -970,21 +1231,11 @@ async function loadCanonicalServers(rootDir: string): Promise<{
|
|
|
970
1231
|
servers: Record<string, unknown>;
|
|
971
1232
|
sourcePath: string | null;
|
|
972
1233
|
}> {
|
|
973
|
-
const
|
|
974
|
-
const
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
return { servers: {}, sourcePath: null };
|
|
979
|
-
}
|
|
980
|
-
try {
|
|
981
|
-
const txt = await Bun.file(preferred).text();
|
|
982
|
-
const parsed = JSON.parse(txt) as unknown;
|
|
983
|
-
const servers = extractServersObject(parsed) ?? {};
|
|
984
|
-
return { servers, sourcePath: preferred };
|
|
985
|
-
} catch {
|
|
986
|
-
return { servers: {}, sourcePath: preferred };
|
|
987
|
-
}
|
|
1234
|
+
const loaded = await loadCanonicalMcpState(rootDir);
|
|
1235
|
+
const sourcePath = (await fileExists(loaded.trackedPath))
|
|
1236
|
+
? loaded.trackedPath
|
|
1237
|
+
: null;
|
|
1238
|
+
return { servers: loaded.trackedServers, sourcePath };
|
|
988
1239
|
}
|
|
989
1240
|
|
|
990
1241
|
async function ensureEmptyDir(p: string) {
|
|
@@ -1200,6 +1451,8 @@ interface ExistingManagedItem {
|
|
|
1200
1451
|
| "skill"
|
|
1201
1452
|
| "agent"
|
|
1202
1453
|
| "automation"
|
|
1454
|
+
| "plugin"
|
|
1455
|
+
| "plugin-marketplace"
|
|
1203
1456
|
| "global-doc"
|
|
1204
1457
|
| "rule"
|
|
1205
1458
|
| "tool-config"
|
|
@@ -1744,12 +1997,6 @@ async function adoptExistingToolConfig(args: {
|
|
|
1744
1997
|
];
|
|
1745
1998
|
}
|
|
1746
1999
|
|
|
1747
|
-
function normalizeCanonicalMcpServers(
|
|
1748
|
-
servers: Record<string, unknown>
|
|
1749
|
-
): string {
|
|
1750
|
-
return JSON.stringify({ servers }, null, 2);
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
2000
|
async function planExistingMcpAdoption(args: {
|
|
1754
2001
|
rootDir: string;
|
|
1755
2002
|
tool: string;
|
|
@@ -1845,7 +2092,7 @@ async function adoptExistingMcpServers(args: {
|
|
|
1845
2092
|
const canonicalPath =
|
|
1846
2093
|
canonical.sourcePath ?? join(args.rootDir, "mcp", "servers.json");
|
|
1847
2094
|
await ensureDir(dirname(canonicalPath));
|
|
1848
|
-
await Bun.write(canonicalPath,
|
|
2095
|
+
await Bun.write(canonicalPath, stringifyCanonicalMcpServers(merged));
|
|
1849
2096
|
return adopted;
|
|
1850
2097
|
}
|
|
1851
2098
|
|
|
@@ -2036,16 +2283,25 @@ async function syncSkillSymlinks({
|
|
|
2036
2283
|
}
|
|
2037
2284
|
|
|
2038
2285
|
async function planMcpWrite({
|
|
2286
|
+
homeDir,
|
|
2039
2287
|
mcpConfigPath,
|
|
2040
2288
|
rootDir,
|
|
2041
2289
|
tool,
|
|
2042
2290
|
}: {
|
|
2291
|
+
homeDir: string;
|
|
2043
2292
|
mcpConfigPath: string;
|
|
2044
2293
|
rootDir: string;
|
|
2045
2294
|
tool: string;
|
|
2046
2295
|
}): Promise<{ needsWrite: boolean; contents: string }> {
|
|
2047
|
-
const { servers } = await
|
|
2048
|
-
|
|
2296
|
+
const { servers } = await loadCanonicalMcpState(rootDir, {
|
|
2297
|
+
includeLocal: true,
|
|
2298
|
+
});
|
|
2299
|
+
const filtered = await filterServersForTool({
|
|
2300
|
+
homeDir,
|
|
2301
|
+
rootDir,
|
|
2302
|
+
servers,
|
|
2303
|
+
tool,
|
|
2304
|
+
});
|
|
2049
2305
|
const contents = `${JSON.stringify({ mcpServers: filtered }, null, 2)}\n`;
|
|
2050
2306
|
|
|
2051
2307
|
if (!(await fileExists(mcpConfigPath))) {
|
|
@@ -2060,17 +2316,19 @@ async function planMcpWrite({
|
|
|
2060
2316
|
}
|
|
2061
2317
|
|
|
2062
2318
|
async function syncMcpConfig({
|
|
2319
|
+
homeDir,
|
|
2063
2320
|
mcpConfigPath,
|
|
2064
2321
|
rootDir,
|
|
2065
2322
|
tool,
|
|
2066
2323
|
dryRun,
|
|
2067
2324
|
}: {
|
|
2325
|
+
homeDir: string;
|
|
2068
2326
|
mcpConfigPath: string;
|
|
2069
2327
|
rootDir: string;
|
|
2070
2328
|
tool: string;
|
|
2071
2329
|
dryRun?: boolean;
|
|
2072
2330
|
}): Promise<{ needsWrite: boolean }> {
|
|
2073
|
-
const plan = await planMcpWrite({ mcpConfigPath, rootDir, tool });
|
|
2331
|
+
const plan = await planMcpWrite({ homeDir, mcpConfigPath, rootDir, tool });
|
|
2074
2332
|
if (dryRun) {
|
|
2075
2333
|
return { needsWrite: plan.needsWrite };
|
|
2076
2334
|
}
|
|
@@ -2082,16 +2340,25 @@ async function syncMcpConfig({
|
|
|
2082
2340
|
}
|
|
2083
2341
|
|
|
2084
2342
|
async function writeToolMcpConfig({
|
|
2343
|
+
homeDir,
|
|
2085
2344
|
mcpConfigPath,
|
|
2086
2345
|
rootDir,
|
|
2087
2346
|
tool,
|
|
2088
2347
|
}: {
|
|
2348
|
+
homeDir: string;
|
|
2089
2349
|
mcpConfigPath: string;
|
|
2090
2350
|
rootDir: string;
|
|
2091
2351
|
tool: string;
|
|
2092
2352
|
}) {
|
|
2093
|
-
const { servers } = await
|
|
2094
|
-
|
|
2353
|
+
const { servers } = await loadCanonicalMcpState(rootDir, {
|
|
2354
|
+
includeLocal: true,
|
|
2355
|
+
});
|
|
2356
|
+
const filtered = await filterServersForTool({
|
|
2357
|
+
homeDir,
|
|
2358
|
+
rootDir,
|
|
2359
|
+
servers,
|
|
2360
|
+
tool,
|
|
2361
|
+
});
|
|
2095
2362
|
await ensureDir(dirname(mcpConfigPath));
|
|
2096
2363
|
await Bun.write(
|
|
2097
2364
|
mcpConfigPath,
|
|
@@ -2113,19 +2380,34 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2113
2380
|
throw new Error(`Unknown tool: ${tool}`);
|
|
2114
2381
|
}
|
|
2115
2382
|
|
|
2116
|
-
const existingSkillPlan =
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2383
|
+
const existingSkillPlan =
|
|
2384
|
+
toolPaths.skillsDir || tool === "codex"
|
|
2385
|
+
? mergeManagedImportPlans(
|
|
2386
|
+
asManagedSkillPlan(
|
|
2387
|
+
toolPaths.skillsDir
|
|
2388
|
+
? await planExistingToolSkillAdoption({
|
|
2389
|
+
rootDir,
|
|
2390
|
+
toolSkillsDir: toolPaths.skillsDir,
|
|
2391
|
+
})
|
|
2392
|
+
: {
|
|
2393
|
+
adopt: [],
|
|
2394
|
+
identical: [],
|
|
2395
|
+
conflicts: [],
|
|
2396
|
+
ignored: [],
|
|
2397
|
+
}
|
|
2398
|
+
),
|
|
2399
|
+
tool === "codex"
|
|
2400
|
+
? asManagedSkillPlan(
|
|
2401
|
+
await planExistingToolSkillAdoption({
|
|
2402
|
+
rootDir,
|
|
2403
|
+
toolSkillsDir: codexLegacySkillsDir(home, rootDir),
|
|
2404
|
+
})
|
|
2405
|
+
)
|
|
2406
|
+
: emptyManagedImportPlan()
|
|
2407
|
+
)
|
|
2408
|
+
: emptyManagedImportPlan();
|
|
2127
2409
|
const existingImportPlan = mergeManagedImportPlans(
|
|
2128
|
-
|
|
2410
|
+
existingSkillPlan,
|
|
2129
2411
|
toolPaths.agentsDir
|
|
2130
2412
|
? await planExistingToolAgentAdoption({
|
|
2131
2413
|
tool,
|
|
@@ -2160,6 +2442,15 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2160
2442
|
toolConfigPath: toolPaths.toolConfig,
|
|
2161
2443
|
})
|
|
2162
2444
|
: emptyManagedImportPlan(),
|
|
2445
|
+
tool === "codex" &&
|
|
2446
|
+
(toolPaths.pluginsDir || toolPaths.pluginMarketplacePath)
|
|
2447
|
+
? await planExistingCodexPluginAdoption({
|
|
2448
|
+
homeDir: home,
|
|
2449
|
+
rootDir,
|
|
2450
|
+
pluginsDir: toolPaths.pluginsDir,
|
|
2451
|
+
pluginMarketplacePath: toolPaths.pluginMarketplacePath,
|
|
2452
|
+
})
|
|
2453
|
+
: emptyManagedImportPlan(),
|
|
2163
2454
|
toolPaths.mcpConfig
|
|
2164
2455
|
? await planExistingMcpAdoption({
|
|
2165
2456
|
rootDir,
|
|
@@ -2181,6 +2472,8 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2181
2472
|
toolPaths.toolHome ||
|
|
2182
2473
|
toolPaths.rulesDir ||
|
|
2183
2474
|
toolPaths.toolConfig ||
|
|
2475
|
+
toolPaths.pluginsDir ||
|
|
2476
|
+
toolPaths.pluginMarketplacePath ||
|
|
2184
2477
|
toolPaths.mcpConfig) &&
|
|
2185
2478
|
!opts.adoptExisting &&
|
|
2186
2479
|
(existingImportPlan.adopt.length > 0 ||
|
|
@@ -2227,7 +2520,10 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2227
2520
|
? await adoptSkillsIntoCanonicalStore({
|
|
2228
2521
|
homeDir: home,
|
|
2229
2522
|
rootDir,
|
|
2230
|
-
skillSourceDirs: [
|
|
2523
|
+
skillSourceDirs: [
|
|
2524
|
+
toolPaths.skillsDir,
|
|
2525
|
+
...(tool === "codex" ? [codexLegacySkillsDir(home, rootDir)] : []),
|
|
2526
|
+
],
|
|
2231
2527
|
})
|
|
2232
2528
|
: [];
|
|
2233
2529
|
|
|
@@ -2250,6 +2546,26 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2250
2546
|
});
|
|
2251
2547
|
}
|
|
2252
2548
|
}
|
|
2549
|
+
if (tool === "codex" && opts.adoptExisting) {
|
|
2550
|
+
const legacySkillsDir = codexLegacySkillsDir(home, rootDir);
|
|
2551
|
+
const result = await adoptExistingToolSkills({
|
|
2552
|
+
rootDir,
|
|
2553
|
+
toolSkillsDir: legacySkillsDir,
|
|
2554
|
+
conflictMode: importConflictMode,
|
|
2555
|
+
});
|
|
2556
|
+
for (const name of result.adopted) {
|
|
2557
|
+
if (!adoptedSkills.includes(name)) {
|
|
2558
|
+
adoptedSkills.push(name);
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
if (result.adopted.length > 0) {
|
|
2562
|
+
await buildIndex({
|
|
2563
|
+
homeDir: home,
|
|
2564
|
+
rootDir,
|
|
2565
|
+
force: false,
|
|
2566
|
+
});
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2253
2569
|
if (toolPaths.agentsDir && opts.adoptExisting) {
|
|
2254
2570
|
const result = await adoptExistingToolAgents({
|
|
2255
2571
|
tool,
|
|
@@ -2303,6 +2619,20 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2303
2619
|
});
|
|
2304
2620
|
adoptedSkills.push(...result.map((item) => `${item.kind}:${item.name}`));
|
|
2305
2621
|
}
|
|
2622
|
+
if (
|
|
2623
|
+
tool === "codex" &&
|
|
2624
|
+
opts.adoptExisting &&
|
|
2625
|
+
(toolPaths.pluginsDir || toolPaths.pluginMarketplacePath)
|
|
2626
|
+
) {
|
|
2627
|
+
const result = await adoptExistingCodexPlugins({
|
|
2628
|
+
homeDir: home,
|
|
2629
|
+
rootDir,
|
|
2630
|
+
pluginsDir: toolPaths.pluginsDir,
|
|
2631
|
+
pluginMarketplacePath: toolPaths.pluginMarketplacePath,
|
|
2632
|
+
conflictMode: importConflictMode,
|
|
2633
|
+
});
|
|
2634
|
+
adoptedSkills.push(...result.map((item) => `${item.kind}:${item.name}`));
|
|
2635
|
+
}
|
|
2306
2636
|
if (adoptedSkills.length > 0) {
|
|
2307
2637
|
await buildIndex({
|
|
2308
2638
|
homeDir: home,
|
|
@@ -2351,6 +2681,14 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2351
2681
|
toolConfigPath: toolPaths.toolConfig,
|
|
2352
2682
|
})
|
|
2353
2683
|
: null;
|
|
2684
|
+
const pluginPreview =
|
|
2685
|
+
tool === "codex" && toolPaths.pluginsDir && toolPaths.pluginMarketplacePath
|
|
2686
|
+
? await planCodexPluginFileChanges({
|
|
2687
|
+
rootDir,
|
|
2688
|
+
pluginsDir: toolPaths.pluginsDir,
|
|
2689
|
+
pluginMarketplacePath: toolPaths.pluginMarketplacePath,
|
|
2690
|
+
})
|
|
2691
|
+
: null;
|
|
2354
2692
|
|
|
2355
2693
|
const skillsBackup = toolPaths.skillsDir
|
|
2356
2694
|
? await backupPath(toolPaths.skillsDir, opts.now)
|
|
@@ -2381,6 +2719,15 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2381
2719
|
toolPaths.toolConfig && toolConfigPreview?.managedConfig
|
|
2382
2720
|
? await backupPath(toolPaths.toolConfig, opts.now)
|
|
2383
2721
|
: null;
|
|
2722
|
+
const pluginsBackup =
|
|
2723
|
+
toolPaths.pluginsDir && pluginPreview?.contents.size
|
|
2724
|
+
? await backupPath(toolPaths.pluginsDir, opts.now)
|
|
2725
|
+
: null;
|
|
2726
|
+
const pluginMarketplaceBackup =
|
|
2727
|
+
toolPaths.pluginMarketplacePath &&
|
|
2728
|
+
pluginPreview?.contents.has(toolPaths.pluginMarketplacePath)
|
|
2729
|
+
? await backupPath(toolPaths.pluginMarketplacePath, opts.now)
|
|
2730
|
+
: null;
|
|
2384
2731
|
|
|
2385
2732
|
if (toolPaths.skillsDir) {
|
|
2386
2733
|
await ensureEmptyDir(toolPaths.skillsDir);
|
|
@@ -2402,6 +2749,7 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2402
2749
|
|
|
2403
2750
|
if (toolPaths.mcpConfig) {
|
|
2404
2751
|
await writeToolMcpConfig({
|
|
2752
|
+
homeDir: home,
|
|
2405
2753
|
mcpConfigPath: toolPaths.mcpConfig,
|
|
2406
2754
|
rootDir,
|
|
2407
2755
|
tool,
|
|
@@ -2457,12 +2805,41 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2457
2805
|
});
|
|
2458
2806
|
}
|
|
2459
2807
|
|
|
2808
|
+
if (
|
|
2809
|
+
pluginPreview &&
|
|
2810
|
+
toolPaths.pluginsDir &&
|
|
2811
|
+
(pluginPreview.contents.size > 0 || pluginPreview.remove.length > 0)
|
|
2812
|
+
) {
|
|
2813
|
+
await ensureDir(toolPaths.pluginsDir);
|
|
2814
|
+
if (
|
|
2815
|
+
toolPaths.pluginMarketplacePath &&
|
|
2816
|
+
pluginPreview.contents.has(toolPaths.pluginMarketplacePath)
|
|
2817
|
+
) {
|
|
2818
|
+
await ensureDir(dirname(toolPaths.pluginMarketplacePath));
|
|
2819
|
+
}
|
|
2820
|
+
await applyRenderedRemoves(pluginPreview.remove);
|
|
2821
|
+
await applyRenderedWrites({
|
|
2822
|
+
contents: pluginPreview.contents,
|
|
2823
|
+
targets: Array.from(pluginPreview.contents.keys()),
|
|
2824
|
+
});
|
|
2825
|
+
await pruneEmptyParents(pluginPreview.remove, toolPaths.pluginsDir);
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2460
2828
|
state.tools[tool] = {
|
|
2461
2829
|
tool,
|
|
2462
2830
|
managedAt: nowIso(opts.now),
|
|
2463
2831
|
skillsDir: toolPaths.skillsDir,
|
|
2464
2832
|
mcpConfig: toolPaths.mcpConfig,
|
|
2465
2833
|
agentsDir: toolPaths.agentsDir,
|
|
2834
|
+
pluginsDir:
|
|
2835
|
+
pluginPreview?.contents.size && toolPaths.pluginsDir
|
|
2836
|
+
? toolPaths.pluginsDir
|
|
2837
|
+
: undefined,
|
|
2838
|
+
pluginMarketplacePath:
|
|
2839
|
+
toolPaths.pluginMarketplacePath &&
|
|
2840
|
+
pluginPreview?.contents.has(toolPaths.pluginMarketplacePath)
|
|
2841
|
+
? toolPaths.pluginMarketplacePath
|
|
2842
|
+
: undefined,
|
|
2466
2843
|
automationDir: toolPaths.automationDir,
|
|
2467
2844
|
toolHome: globalDocsPreview?.managedTargets.length
|
|
2468
2845
|
? toolPaths.toolHome
|
|
@@ -2484,6 +2861,8 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2484
2861
|
skillsBackup,
|
|
2485
2862
|
mcpBackup,
|
|
2486
2863
|
agentsBackup,
|
|
2864
|
+
pluginsBackup,
|
|
2865
|
+
pluginMarketplaceBackup,
|
|
2487
2866
|
globalAgentsBackup,
|
|
2488
2867
|
globalAgentsOverrideBackup,
|
|
2489
2868
|
rulesBackup,
|
|
@@ -2549,6 +2928,17 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2549
2928
|
});
|
|
2550
2929
|
}
|
|
2551
2930
|
|
|
2931
|
+
if (pluginPreview) {
|
|
2932
|
+
updateRenderedTargetState({
|
|
2933
|
+
entry: managedEntry,
|
|
2934
|
+
writtenTargets: Array.from(pluginPreview.contents.keys()),
|
|
2935
|
+
removedTargets: pluginPreview.remove,
|
|
2936
|
+
contents: pluginPreview.contents,
|
|
2937
|
+
sources: pluginPreview.sources,
|
|
2938
|
+
normalizeText: false,
|
|
2939
|
+
});
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2552
2942
|
await saveManagedState(state, home, rootDir);
|
|
2553
2943
|
|
|
2554
2944
|
for (const name of adoptedSkills) {
|
|
@@ -2625,6 +3015,20 @@ export async function unmanageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2625
3015
|
});
|
|
2626
3016
|
}
|
|
2627
3017
|
|
|
3018
|
+
if (entry.pluginsDir) {
|
|
3019
|
+
await restoreBackup({
|
|
3020
|
+
original: entry.pluginsDir,
|
|
3021
|
+
backup: entry.pluginsBackup ?? null,
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
if (entry.pluginMarketplacePath) {
|
|
3026
|
+
await restoreBackup({
|
|
3027
|
+
original: entry.pluginMarketplacePath,
|
|
3028
|
+
backup: entry.pluginMarketplaceBackup ?? null,
|
|
3029
|
+
});
|
|
3030
|
+
}
|
|
3031
|
+
|
|
2628
3032
|
if (entry.automationDir) {
|
|
2629
3033
|
const automationTargets = Object.keys(entry.renderedTargets ?? {}).filter(
|
|
2630
3034
|
(targetPath) => targetPath.startsWith(join(entry.automationDir!, ""))
|
|
@@ -2705,6 +3109,19 @@ async function repairManagedToolEntry(args: {
|
|
|
2705
3109
|
const next: ManagedToolState = { ...args.entry };
|
|
2706
3110
|
let changed = false;
|
|
2707
3111
|
|
|
3112
|
+
if (
|
|
3113
|
+
tool === "codex" &&
|
|
3114
|
+
toolPaths.skillsDir &&
|
|
3115
|
+
(await canonicalSkillsExist(rootDir)) &&
|
|
3116
|
+
next.skillsDir !== toolPaths.skillsDir
|
|
3117
|
+
) {
|
|
3118
|
+
if (!next.skillsBackup) {
|
|
3119
|
+
next.skillsBackup = await backupPath(toolPaths.skillsDir);
|
|
3120
|
+
}
|
|
3121
|
+
next.skillsDir = toolPaths.skillsDir;
|
|
3122
|
+
changed = true;
|
|
3123
|
+
}
|
|
3124
|
+
|
|
2708
3125
|
if (
|
|
2709
3126
|
!next.agentsDir &&
|
|
2710
3127
|
toolPaths.agentsDir &&
|
|
@@ -2724,6 +3141,43 @@ async function repairManagedToolEntry(args: {
|
|
|
2724
3141
|
changed = true;
|
|
2725
3142
|
}
|
|
2726
3143
|
|
|
3144
|
+
if (
|
|
3145
|
+
tool === "codex" &&
|
|
3146
|
+
!(next.pluginsDir && next.pluginMarketplacePath) &&
|
|
3147
|
+
toolPaths.pluginsDir &&
|
|
3148
|
+
toolPaths.pluginMarketplacePath &&
|
|
3149
|
+
(await canonicalCodexPluginsExist(rootDir))
|
|
3150
|
+
) {
|
|
3151
|
+
if (!next.pluginsDir) {
|
|
3152
|
+
next.pluginsBackup = await backupPath(toolPaths.pluginsDir);
|
|
3153
|
+
next.pluginsDir = toolPaths.pluginsDir;
|
|
3154
|
+
changed = true;
|
|
3155
|
+
}
|
|
3156
|
+
if (!next.pluginMarketplacePath) {
|
|
3157
|
+
next.pluginMarketplaceBackup = await backupPath(
|
|
3158
|
+
toolPaths.pluginMarketplacePath
|
|
3159
|
+
);
|
|
3160
|
+
next.pluginMarketplacePath = toolPaths.pluginMarketplacePath;
|
|
3161
|
+
changed = true;
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
if (
|
|
3166
|
+
tool === "codex" &&
|
|
3167
|
+
!toolPaths.pluginsDir &&
|
|
3168
|
+
!toolPaths.pluginMarketplacePath &&
|
|
3169
|
+
(next.pluginsDir ||
|
|
3170
|
+
next.pluginMarketplacePath ||
|
|
3171
|
+
next.pluginsBackup ||
|
|
3172
|
+
next.pluginMarketplaceBackup)
|
|
3173
|
+
) {
|
|
3174
|
+
next.pluginsDir = undefined;
|
|
3175
|
+
next.pluginMarketplacePath = undefined;
|
|
3176
|
+
next.pluginsBackup = undefined;
|
|
3177
|
+
next.pluginMarketplaceBackup = undefined;
|
|
3178
|
+
changed = true;
|
|
3179
|
+
}
|
|
3180
|
+
|
|
2727
3181
|
if (toolPaths.toolHome && !next.toolHome) {
|
|
2728
3182
|
const preview = await syncToolGlobalDocs({
|
|
2729
3183
|
homeDir,
|
|
@@ -2807,10 +3261,11 @@ async function planRenderedTargetConflicts(args: {
|
|
|
2807
3261
|
entry: ManagedToolState;
|
|
2808
3262
|
desiredWrites: string[];
|
|
2809
3263
|
desiredRemoves: string[];
|
|
2810
|
-
desiredContents: Map<string,
|
|
3264
|
+
desiredContents: Map<string, ManagedTargetContent>;
|
|
2811
3265
|
desiredSources: Map<string, string>;
|
|
2812
3266
|
conflictMode?: "warn" | "overwrite";
|
|
2813
3267
|
protectAllSources?: boolean;
|
|
3268
|
+
normalizeText?: boolean;
|
|
2814
3269
|
}): Promise<RenderedApplyPlan> {
|
|
2815
3270
|
if (args.conflictMode === "overwrite") {
|
|
2816
3271
|
return {
|
|
@@ -2848,17 +3303,19 @@ async function planRenderedTargetConflicts(args: {
|
|
|
2848
3303
|
}
|
|
2849
3304
|
|
|
2850
3305
|
const prior = previous[targetPath];
|
|
2851
|
-
const
|
|
2852
|
-
|
|
3306
|
+
const currentHash = await readTargetHash(targetPath, {
|
|
3307
|
+
normalizeText: args.normalizeText,
|
|
3308
|
+
});
|
|
3309
|
+
if (currentHash == null) {
|
|
2853
3310
|
if (args.desiredWrites.includes(targetPath)) {
|
|
2854
3311
|
write.push(targetPath);
|
|
2855
3312
|
}
|
|
2856
3313
|
continue;
|
|
2857
3314
|
}
|
|
2858
|
-
|
|
2859
|
-
const currentHash = renderedHash(current);
|
|
2860
3315
|
const desiredHash = args.desiredContents.get(targetPath)
|
|
2861
|
-
?
|
|
3316
|
+
? targetContentHash(args.desiredContents.get(targetPath)!, {
|
|
3317
|
+
normalizeText: args.normalizeText,
|
|
3318
|
+
})
|
|
2862
3319
|
: null;
|
|
2863
3320
|
if (prior?.hash) {
|
|
2864
3321
|
if (
|
|
@@ -2931,7 +3388,7 @@ function logRenderedConflicts(
|
|
|
2931
3388
|
}
|
|
2932
3389
|
|
|
2933
3390
|
async function applyRenderedWrites(args: {
|
|
2934
|
-
contents: Map<string,
|
|
3391
|
+
contents: Map<string, ManagedTargetContent>;
|
|
2935
3392
|
targets: string[];
|
|
2936
3393
|
}) {
|
|
2937
3394
|
for (const pathValue of args.targets) {
|
|
@@ -2942,7 +3399,9 @@ async function applyRenderedWrites(args: {
|
|
|
2942
3399
|
await mkdir(dirname(pathValue), { recursive: true });
|
|
2943
3400
|
await Bun.write(
|
|
2944
3401
|
pathValue,
|
|
2945
|
-
desired.endsWith("\n")
|
|
3402
|
+
typeof desired === "string" && !desired.endsWith("\n")
|
|
3403
|
+
? `${desired}\n`
|
|
3404
|
+
: desired
|
|
2946
3405
|
);
|
|
2947
3406
|
}
|
|
2948
3407
|
}
|
|
@@ -2975,8 +3434,9 @@ function updateRenderedTargetState(args: {
|
|
|
2975
3434
|
entry: ManagedToolState;
|
|
2976
3435
|
writtenTargets: string[];
|
|
2977
3436
|
removedTargets: string[];
|
|
2978
|
-
contents: Map<string,
|
|
3437
|
+
contents: Map<string, ManagedTargetContent>;
|
|
2979
3438
|
sources: Map<string, string>;
|
|
3439
|
+
normalizeText?: boolean;
|
|
2980
3440
|
}) {
|
|
2981
3441
|
const next = { ...(args.entry.renderedTargets ?? {}) };
|
|
2982
3442
|
for (const pathValue of args.removedTargets) {
|
|
@@ -2989,7 +3449,9 @@ function updateRenderedTargetState(args: {
|
|
|
2989
3449
|
continue;
|
|
2990
3450
|
}
|
|
2991
3451
|
next[pathValue] = {
|
|
2992
|
-
hash:
|
|
3452
|
+
hash: targetContentHash(contents, {
|
|
3453
|
+
normalizeText: args.normalizeText,
|
|
3454
|
+
}),
|
|
2993
3455
|
sourcePath,
|
|
2994
3456
|
sourceKind: renderedSourceKindForPath(sourcePath),
|
|
2995
3457
|
};
|
|
@@ -3036,6 +3498,8 @@ function logSyncDryRun({
|
|
|
3036
3498
|
rulesConflicts,
|
|
3037
3499
|
configPlan,
|
|
3038
3500
|
configConflicts,
|
|
3501
|
+
pluginPlan,
|
|
3502
|
+
pluginConflicts,
|
|
3039
3503
|
}: {
|
|
3040
3504
|
tool: string;
|
|
3041
3505
|
entry: ManagedToolState;
|
|
@@ -3051,6 +3515,8 @@ function logSyncDryRun({
|
|
|
3051
3515
|
rulesConflicts: RenderedConflict[];
|
|
3052
3516
|
configPlan: { write: boolean; remove: boolean; targetPath: string };
|
|
3053
3517
|
configConflicts: RenderedConflict[];
|
|
3518
|
+
pluginPlan: { write: string[]; remove: string[] };
|
|
3519
|
+
pluginConflicts: RenderedConflict[];
|
|
3054
3520
|
}) {
|
|
3055
3521
|
for (const name of skillPlan.add) {
|
|
3056
3522
|
console.log(`${tool}: would add skill ${name}`);
|
|
@@ -3093,6 +3559,13 @@ function logSyncDryRun({
|
|
|
3093
3559
|
console.log(`${tool}: would remove tool config ${configPlan.targetPath}`);
|
|
3094
3560
|
}
|
|
3095
3561
|
logRenderedConflicts(tool, configConflicts, true);
|
|
3562
|
+
for (const p of pluginPlan.write) {
|
|
3563
|
+
console.log(`${tool}: would write plugin asset ${p}`);
|
|
3564
|
+
}
|
|
3565
|
+
for (const p of pluginPlan.remove) {
|
|
3566
|
+
console.log(`${tool}: would remove plugin asset ${p}`);
|
|
3567
|
+
}
|
|
3568
|
+
logRenderedConflicts(tool, pluginConflicts, true);
|
|
3096
3569
|
if (mcpPlan.needsWrite && entry.mcpConfig) {
|
|
3097
3570
|
console.log(`${tool}: would update mcp config ${entry.mcpConfig}`);
|
|
3098
3571
|
}
|
|
@@ -3109,12 +3582,15 @@ function logSyncDryRun({
|
|
|
3109
3582
|
rulesPlan.remove.length === 0 &&
|
|
3110
3583
|
!configPlan.write &&
|
|
3111
3584
|
!configPlan.remove &&
|
|
3585
|
+
pluginPlan.write.length === 0 &&
|
|
3586
|
+
pluginPlan.remove.length === 0 &&
|
|
3112
3587
|
!mcpPlan.needsWrite &&
|
|
3113
3588
|
agentConflicts.length === 0 &&
|
|
3114
3589
|
automationConflicts.length === 0 &&
|
|
3115
3590
|
globalDocsConflicts.length === 0 &&
|
|
3116
3591
|
rulesConflicts.length === 0 &&
|
|
3117
|
-
configConflicts.length === 0
|
|
3592
|
+
configConflicts.length === 0 &&
|
|
3593
|
+
pluginConflicts.length === 0
|
|
3118
3594
|
) {
|
|
3119
3595
|
console.log(`${tool}: no changes`);
|
|
3120
3596
|
}
|
|
@@ -3199,6 +3675,24 @@ async function repairManagedCanonicalContent(args: {
|
|
|
3199
3675
|
adopted.push(...items.map((item) => `${item.kind}:${item.name}`));
|
|
3200
3676
|
}
|
|
3201
3677
|
|
|
3678
|
+
if (
|
|
3679
|
+
args.tool === "codex" &&
|
|
3680
|
+
(args.entry.pluginsBackup ||
|
|
3681
|
+
args.entry.pluginsDir ||
|
|
3682
|
+
args.entry.pluginMarketplaceBackup ||
|
|
3683
|
+
args.entry.pluginMarketplacePath)
|
|
3684
|
+
) {
|
|
3685
|
+
const items = await adoptExistingCodexPlugins({
|
|
3686
|
+
homeDir: args.homeDir,
|
|
3687
|
+
rootDir: args.rootDir,
|
|
3688
|
+
pluginsDir: args.entry.pluginsBackup ?? args.entry.pluginsDir,
|
|
3689
|
+
pluginMarketplacePath:
|
|
3690
|
+
args.entry.pluginMarketplaceBackup ?? args.entry.pluginMarketplacePath,
|
|
3691
|
+
conflictMode: "keep-canonical",
|
|
3692
|
+
});
|
|
3693
|
+
adopted.push(...items.map((item) => `${item.kind}:${item.name}`));
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3202
3696
|
if (adopted.length > 0) {
|
|
3203
3697
|
await buildIndex({
|
|
3204
3698
|
homeDir: args.homeDir,
|
|
@@ -3210,6 +3704,289 @@ async function repairManagedCanonicalContent(args: {
|
|
|
3210
3704
|
return adopted;
|
|
3211
3705
|
}
|
|
3212
3706
|
|
|
3707
|
+
async function discoverExistingCodexPluginEntries(args: {
|
|
3708
|
+
homeDir: string;
|
|
3709
|
+
rootDir: string;
|
|
3710
|
+
pluginMarketplacePath?: string;
|
|
3711
|
+
pluginsDir?: string;
|
|
3712
|
+
}): Promise<
|
|
3713
|
+
{
|
|
3714
|
+
name: string;
|
|
3715
|
+
livePath: string;
|
|
3716
|
+
sourcePath: string;
|
|
3717
|
+
}[]
|
|
3718
|
+
> {
|
|
3719
|
+
if (projectRootFromAiRoot(args.rootDir, args.homeDir) != null) {
|
|
3720
|
+
return [];
|
|
3721
|
+
}
|
|
3722
|
+
|
|
3723
|
+
const results = new Map<
|
|
3724
|
+
string,
|
|
3725
|
+
{ name: string; livePath: string; sourcePath: string }
|
|
3726
|
+
>();
|
|
3727
|
+
const liveRoot = codexLiveRoot(args.homeDir, args.rootDir);
|
|
3728
|
+
const marketplaceRaw = args.pluginMarketplacePath
|
|
3729
|
+
? await readTextOrNull(args.pluginMarketplacePath)
|
|
3730
|
+
: null;
|
|
3731
|
+
if (marketplaceRaw) {
|
|
3732
|
+
try {
|
|
3733
|
+
const parsed = JSON.parse(marketplaceRaw) as unknown;
|
|
3734
|
+
const plugins =
|
|
3735
|
+
isPlainObject(parsed) && Array.isArray(parsed.plugins)
|
|
3736
|
+
? parsed.plugins
|
|
3737
|
+
: [];
|
|
3738
|
+
for (const entry of plugins) {
|
|
3739
|
+
if (
|
|
3740
|
+
!isPlainObject(entry) ||
|
|
3741
|
+
typeof entry.name !== "string" ||
|
|
3742
|
+
!isSafeCodexPluginName(entry.name)
|
|
3743
|
+
) {
|
|
3744
|
+
continue;
|
|
3745
|
+
}
|
|
3746
|
+
const source = isPlainObject(entry.source) ? entry.source : null;
|
|
3747
|
+
if (!(source?.source === "local" && typeof source.path === "string")) {
|
|
3748
|
+
continue;
|
|
3749
|
+
}
|
|
3750
|
+
const pathValue = source.path.trim();
|
|
3751
|
+
if (
|
|
3752
|
+
!(
|
|
3753
|
+
pathValue === `./plugins/${entry.name}` ||
|
|
3754
|
+
pathValue === `./.codex/plugins/${entry.name}`
|
|
3755
|
+
)
|
|
3756
|
+
) {
|
|
3757
|
+
continue;
|
|
3758
|
+
}
|
|
3759
|
+
const livePath = join(liveRoot, pathValue.slice(2));
|
|
3760
|
+
if (
|
|
3761
|
+
!(await fileExists(join(livePath, ".codex-plugin", "plugin.json")))
|
|
3762
|
+
) {
|
|
3763
|
+
continue;
|
|
3764
|
+
}
|
|
3765
|
+
results.set(entry.name, {
|
|
3766
|
+
name: entry.name,
|
|
3767
|
+
livePath,
|
|
3768
|
+
sourcePath: pathValue,
|
|
3769
|
+
});
|
|
3770
|
+
}
|
|
3771
|
+
} catch {
|
|
3772
|
+
// Ignore malformed marketplace files during adoption planning.
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
|
|
3776
|
+
for (const candidateRoot of [
|
|
3777
|
+
args.pluginsDir,
|
|
3778
|
+
codexLegacyPluginsDir(args.homeDir, args.rootDir),
|
|
3779
|
+
]) {
|
|
3780
|
+
if (!(candidateRoot && (await fileExists(candidateRoot)))) {
|
|
3781
|
+
continue;
|
|
3782
|
+
}
|
|
3783
|
+
const entries = await readdir(candidateRoot, { withFileTypes: true }).catch(
|
|
3784
|
+
() => [] as import("node:fs").Dirent[]
|
|
3785
|
+
);
|
|
3786
|
+
for (const entry of entries) {
|
|
3787
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
3788
|
+
continue;
|
|
3789
|
+
}
|
|
3790
|
+
const livePath = join(candidateRoot, entry.name);
|
|
3791
|
+
if (!(await fileExists(join(livePath, ".codex-plugin", "plugin.json")))) {
|
|
3792
|
+
continue;
|
|
3793
|
+
}
|
|
3794
|
+
if (results.has(entry.name)) {
|
|
3795
|
+
continue;
|
|
3796
|
+
}
|
|
3797
|
+
const relativePrefix =
|
|
3798
|
+
candidateRoot === args.pluginsDir ? "./plugins/" : "./.codex/plugins/";
|
|
3799
|
+
results.set(entry.name, {
|
|
3800
|
+
name: entry.name,
|
|
3801
|
+
livePath,
|
|
3802
|
+
sourcePath: `${relativePrefix}${entry.name}`,
|
|
3803
|
+
});
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
|
|
3807
|
+
return Array.from(results.values()).sort((a, b) =>
|
|
3808
|
+
a.name.localeCompare(b.name)
|
|
3809
|
+
);
|
|
3810
|
+
}
|
|
3811
|
+
|
|
3812
|
+
async function planExistingCodexPluginAdoption(args: {
|
|
3813
|
+
homeDir: string;
|
|
3814
|
+
rootDir: string;
|
|
3815
|
+
pluginMarketplacePath?: string;
|
|
3816
|
+
pluginsDir?: string;
|
|
3817
|
+
}): Promise<ExistingManagedImportPlan> {
|
|
3818
|
+
const plan = emptyManagedImportPlan();
|
|
3819
|
+
const canonicalMarketplacePath = codexCanonicalPluginMarketplacePath(
|
|
3820
|
+
args.rootDir
|
|
3821
|
+
);
|
|
3822
|
+
const marketplaceRaw = args.pluginMarketplacePath
|
|
3823
|
+
? await readTextOrNull(args.pluginMarketplacePath)
|
|
3824
|
+
: null;
|
|
3825
|
+
if (marketplaceRaw != null) {
|
|
3826
|
+
const normalizedLive = normalizeCodexMarketplaceText(marketplaceRaw);
|
|
3827
|
+
const canonicalRaw = await readTextOrNull(canonicalMarketplacePath);
|
|
3828
|
+
const item: ExistingManagedItem = {
|
|
3829
|
+
kind: "plugin-marketplace",
|
|
3830
|
+
name: "codex/plugins/marketplace.json",
|
|
3831
|
+
livePath: args.pluginMarketplacePath!,
|
|
3832
|
+
canonicalPath: canonicalMarketplacePath,
|
|
3833
|
+
};
|
|
3834
|
+
if (canonicalRaw == null) {
|
|
3835
|
+
plan.adopt.push(item);
|
|
3836
|
+
} else if (normalizeCodexMarketplaceText(canonicalRaw) === normalizedLive) {
|
|
3837
|
+
plan.identical.push(item);
|
|
3838
|
+
} else {
|
|
3839
|
+
plan.conflicts.push(item);
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
|
|
3843
|
+
for (const plugin of await discoverExistingCodexPluginEntries(args)) {
|
|
3844
|
+
const canonicalPath = join(
|
|
3845
|
+
codexCanonicalPluginsRoot(args.rootDir),
|
|
3846
|
+
plugin.name
|
|
3847
|
+
);
|
|
3848
|
+
const canonicalHash = await hashDirectoryTree(canonicalPath);
|
|
3849
|
+
const liveHash = await hashDirectoryTree(plugin.livePath);
|
|
3850
|
+
const item: ExistingManagedItem = {
|
|
3851
|
+
kind: "plugin",
|
|
3852
|
+
name: plugin.name,
|
|
3853
|
+
livePath: plugin.livePath,
|
|
3854
|
+
canonicalPath,
|
|
3855
|
+
};
|
|
3856
|
+
if (canonicalHash == null) {
|
|
3857
|
+
plan.adopt.push(item);
|
|
3858
|
+
} else if (canonicalHash === liveHash) {
|
|
3859
|
+
plan.identical.push(item);
|
|
3860
|
+
} else {
|
|
3861
|
+
plan.conflicts.push(item);
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
|
|
3865
|
+
return mergeManagedImportPlans(plan);
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3868
|
+
async function adoptExistingCodexPlugins(args: {
|
|
3869
|
+
homeDir: string;
|
|
3870
|
+
rootDir: string;
|
|
3871
|
+
pluginMarketplacePath?: string;
|
|
3872
|
+
pluginsDir?: string;
|
|
3873
|
+
conflictMode: "keep-canonical" | "keep-existing";
|
|
3874
|
+
}): Promise<ExistingManagedItem[]> {
|
|
3875
|
+
const adopted: ExistingManagedItem[] = [];
|
|
3876
|
+
const canonicalMarketplacePath = codexCanonicalPluginMarketplacePath(
|
|
3877
|
+
args.rootDir
|
|
3878
|
+
);
|
|
3879
|
+
const marketplaceRaw = args.pluginMarketplacePath
|
|
3880
|
+
? await readTextOrNull(args.pluginMarketplacePath)
|
|
3881
|
+
: null;
|
|
3882
|
+
if (marketplaceRaw != null) {
|
|
3883
|
+
const normalizedLive = normalizeCodexMarketplaceText(marketplaceRaw);
|
|
3884
|
+
const canonicalRaw = await readTextOrNull(canonicalMarketplacePath);
|
|
3885
|
+
if (canonicalRaw == null || args.conflictMode === "keep-existing") {
|
|
3886
|
+
await ensureDir(dirname(canonicalMarketplacePath));
|
|
3887
|
+
await Bun.write(canonicalMarketplacePath, normalizedLive);
|
|
3888
|
+
adopted.push({
|
|
3889
|
+
kind: "plugin-marketplace",
|
|
3890
|
+
name: "codex/plugins/marketplace.json",
|
|
3891
|
+
livePath: args.pluginMarketplacePath!,
|
|
3892
|
+
canonicalPath: canonicalMarketplacePath,
|
|
3893
|
+
});
|
|
3894
|
+
}
|
|
3895
|
+
}
|
|
3896
|
+
|
|
3897
|
+
for (const plugin of await discoverExistingCodexPluginEntries(args)) {
|
|
3898
|
+
const canonicalPath = join(
|
|
3899
|
+
codexCanonicalPluginsRoot(args.rootDir),
|
|
3900
|
+
plugin.name
|
|
3901
|
+
);
|
|
3902
|
+
const canonicalHash = await hashDirectoryTree(canonicalPath);
|
|
3903
|
+
const liveHash = await hashDirectoryTree(plugin.livePath);
|
|
3904
|
+
if (
|
|
3905
|
+
canonicalHash != null &&
|
|
3906
|
+
canonicalHash !== liveHash &&
|
|
3907
|
+
args.conflictMode !== "keep-existing"
|
|
3908
|
+
) {
|
|
3909
|
+
continue;
|
|
3910
|
+
}
|
|
3911
|
+
await ensureDir(dirname(canonicalPath));
|
|
3912
|
+
await rm(canonicalPath, { recursive: true, force: true });
|
|
3913
|
+
await cp(plugin.livePath, canonicalPath, { recursive: true });
|
|
3914
|
+
adopted.push({
|
|
3915
|
+
kind: "plugin",
|
|
3916
|
+
name: plugin.name,
|
|
3917
|
+
livePath: plugin.livePath,
|
|
3918
|
+
canonicalPath,
|
|
3919
|
+
});
|
|
3920
|
+
}
|
|
3921
|
+
|
|
3922
|
+
return adopted;
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3925
|
+
async function planCodexPluginFileChanges(args: {
|
|
3926
|
+
rootDir: string;
|
|
3927
|
+
pluginsDir: string;
|
|
3928
|
+
pluginMarketplacePath?: string;
|
|
3929
|
+
previouslyManagedTargets?: string[];
|
|
3930
|
+
}): Promise<{
|
|
3931
|
+
add: string[];
|
|
3932
|
+
remove: string[];
|
|
3933
|
+
contents: Map<string, ManagedTargetContent>;
|
|
3934
|
+
sources: Map<string, string>;
|
|
3935
|
+
}> {
|
|
3936
|
+
const contents = new Map<string, ManagedTargetContent>();
|
|
3937
|
+
const sources = new Map<string, string>();
|
|
3938
|
+
const desiredPaths = new Set<string>();
|
|
3939
|
+
|
|
3940
|
+
const marketplace = await loadCanonicalCodexMarketplaceText(args.rootDir);
|
|
3941
|
+
if (marketplace.text != null && args.pluginMarketplacePath) {
|
|
3942
|
+
desiredPaths.add(args.pluginMarketplacePath);
|
|
3943
|
+
contents.set(args.pluginMarketplacePath, marketplace.text);
|
|
3944
|
+
sources.set(args.pluginMarketplacePath, marketplace.sourcePath);
|
|
3945
|
+
}
|
|
3946
|
+
|
|
3947
|
+
for (const plugin of await loadCanonicalCodexPlugins(args.rootDir)) {
|
|
3948
|
+
for (const [relPath, bytes] of plugin.files.entries()) {
|
|
3949
|
+
const targetPath = join(args.pluginsDir, plugin.name, relPath);
|
|
3950
|
+
desiredPaths.add(targetPath);
|
|
3951
|
+
contents.set(targetPath, bytes);
|
|
3952
|
+
sources.set(targetPath, join(plugin.sourceDir, relPath));
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
|
|
3956
|
+
const add = new Set<string>();
|
|
3957
|
+
for (const targetPath of desiredPaths) {
|
|
3958
|
+
const currentHash = await readTargetHash(targetPath, {
|
|
3959
|
+
normalizeText: false,
|
|
3960
|
+
});
|
|
3961
|
+
const desired = contents.get(targetPath);
|
|
3962
|
+
if (desired == null) {
|
|
3963
|
+
continue;
|
|
3964
|
+
}
|
|
3965
|
+
if (currentHash !== targetContentHash(desired, { normalizeText: false })) {
|
|
3966
|
+
add.add(targetPath);
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
|
|
3970
|
+
const remove = Array.from(
|
|
3971
|
+
new Set(
|
|
3972
|
+
(args.previouslyManagedTargets ?? []).filter((targetPath) => {
|
|
3973
|
+
const inManagedRoot =
|
|
3974
|
+
(args.pluginMarketplacePath != null &&
|
|
3975
|
+
targetPath === args.pluginMarketplacePath) ||
|
|
3976
|
+
targetPath.startsWith(join(args.pluginsDir, ""));
|
|
3977
|
+
return inManagedRoot && !desiredPaths.has(targetPath);
|
|
3978
|
+
})
|
|
3979
|
+
)
|
|
3980
|
+
).sort();
|
|
3981
|
+
|
|
3982
|
+
return {
|
|
3983
|
+
add: Array.from(add).sort(),
|
|
3984
|
+
remove,
|
|
3985
|
+
contents,
|
|
3986
|
+
sources,
|
|
3987
|
+
};
|
|
3988
|
+
}
|
|
3989
|
+
|
|
3213
3990
|
async function syncManagedToolEntry({
|
|
3214
3991
|
homeDir,
|
|
3215
3992
|
tool,
|
|
@@ -3267,6 +4044,7 @@ async function syncManagedToolEntry({
|
|
|
3267
4044
|
|
|
3268
4045
|
const mcpPlan = entry.mcpConfig
|
|
3269
4046
|
? await syncMcpConfig({
|
|
4047
|
+
homeDir,
|
|
3270
4048
|
mcpConfigPath: entry.mcpConfig,
|
|
3271
4049
|
rootDir,
|
|
3272
4050
|
tool,
|
|
@@ -3326,6 +4104,15 @@ async function syncManagedToolEntry({
|
|
|
3326
4104
|
managedConfig: false,
|
|
3327
4105
|
targetPath: "",
|
|
3328
4106
|
};
|
|
4107
|
+
const pluginPlan =
|
|
4108
|
+
tool === "codex" && entry.pluginsDir
|
|
4109
|
+
? await planCodexPluginFileChanges({
|
|
4110
|
+
rootDir,
|
|
4111
|
+
pluginsDir: entry.pluginsDir,
|
|
4112
|
+
pluginMarketplacePath: entry.pluginMarketplacePath,
|
|
4113
|
+
previouslyManagedTargets: Object.keys(entry.renderedTargets ?? {}),
|
|
4114
|
+
})
|
|
4115
|
+
: { add: [], remove: [], contents: new Map(), sources: new Map() };
|
|
3329
4116
|
|
|
3330
4117
|
const agentRendered = await planRenderedTargetConflicts({
|
|
3331
4118
|
entry,
|
|
@@ -3379,6 +4166,16 @@ async function syncManagedToolEntry({
|
|
|
3379
4166
|
desiredSources: configSources,
|
|
3380
4167
|
conflictMode: builtinConflictMode,
|
|
3381
4168
|
});
|
|
4169
|
+
const pluginRendered = await planRenderedTargetConflicts({
|
|
4170
|
+
entry,
|
|
4171
|
+
desiredWrites: pluginPlan.add,
|
|
4172
|
+
desiredRemoves: pluginPlan.remove,
|
|
4173
|
+
desiredContents: pluginPlan.contents,
|
|
4174
|
+
desiredSources: pluginPlan.sources,
|
|
4175
|
+
conflictMode: builtinConflictMode,
|
|
4176
|
+
protectAllSources: true,
|
|
4177
|
+
normalizeText: false,
|
|
4178
|
+
});
|
|
3382
4179
|
|
|
3383
4180
|
if (dryRun) {
|
|
3384
4181
|
logSyncDryRun({
|
|
@@ -3406,6 +4203,11 @@ async function syncManagedToolEntry({
|
|
|
3406
4203
|
targetPath: configPlan.targetPath,
|
|
3407
4204
|
},
|
|
3408
4205
|
configConflicts: configRendered.conflicts,
|
|
4206
|
+
pluginPlan: {
|
|
4207
|
+
write: pluginRendered.write,
|
|
4208
|
+
remove: pluginRendered.remove,
|
|
4209
|
+
},
|
|
4210
|
+
pluginConflicts: pluginRendered.conflicts,
|
|
3409
4211
|
});
|
|
3410
4212
|
} else {
|
|
3411
4213
|
await applyRenderedRemoves(agentRendered.remove);
|
|
@@ -3436,11 +4238,20 @@ async function syncManagedToolEntry({
|
|
|
3436
4238
|
contents: configContents,
|
|
3437
4239
|
targets: configRendered.write,
|
|
3438
4240
|
});
|
|
4241
|
+
await applyRenderedRemoves(pluginRendered.remove);
|
|
4242
|
+
await applyRenderedWrites({
|
|
4243
|
+
contents: pluginPlan.contents,
|
|
4244
|
+
targets: pluginRendered.write,
|
|
4245
|
+
});
|
|
4246
|
+
if (entry.pluginsDir) {
|
|
4247
|
+
await pruneEmptyParents(pluginRendered.remove, entry.pluginsDir);
|
|
4248
|
+
}
|
|
3439
4249
|
logRenderedConflicts(tool, agentRendered.conflicts);
|
|
3440
4250
|
logRenderedConflicts(tool, automationRendered.conflicts);
|
|
3441
4251
|
logRenderedConflicts(tool, globalDocsRendered.conflicts);
|
|
3442
4252
|
logRenderedConflicts(tool, rulesRendered.conflicts);
|
|
3443
4253
|
logRenderedConflicts(tool, configRendered.conflicts);
|
|
4254
|
+
logRenderedConflicts(tool, pluginRendered.conflicts);
|
|
3444
4255
|
|
|
3445
4256
|
updateRenderedTargetState({
|
|
3446
4257
|
entry,
|
|
@@ -3477,6 +4288,14 @@ async function syncManagedToolEntry({
|
|
|
3477
4288
|
contents: configContents,
|
|
3478
4289
|
sources: configSources,
|
|
3479
4290
|
});
|
|
4291
|
+
updateRenderedTargetState({
|
|
4292
|
+
entry,
|
|
4293
|
+
writtenTargets: pluginRendered.write,
|
|
4294
|
+
removedTargets: pluginRendered.remove,
|
|
4295
|
+
contents: pluginPlan.contents,
|
|
4296
|
+
sources: pluginPlan.sources,
|
|
4297
|
+
normalizeText: false,
|
|
4298
|
+
});
|
|
3480
4299
|
|
|
3481
4300
|
for (const name of adoptedSkills) {
|
|
3482
4301
|
console.log(
|
|
@@ -3667,11 +4486,20 @@ export async function managedCommand(argv: string[] = []) {
|
|
|
3667
4486
|
parsed.argv.includes("-h") ||
|
|
3668
4487
|
parsed.argv[0] === "help"
|
|
3669
4488
|
) {
|
|
3670
|
-
console.log(
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
4489
|
+
console.log(
|
|
4490
|
+
renderPage({
|
|
4491
|
+
title: "fclt managed",
|
|
4492
|
+
subtitle: "List tools currently in managed mode.",
|
|
4493
|
+
sections: [
|
|
4494
|
+
{
|
|
4495
|
+
title: "Usage",
|
|
4496
|
+
lines: renderBullets([
|
|
4497
|
+
renderCode("fclt managed [--root PATH|--global|--project]"),
|
|
4498
|
+
]),
|
|
4499
|
+
},
|
|
4500
|
+
],
|
|
4501
|
+
})
|
|
4502
|
+
);
|
|
3675
4503
|
return;
|
|
3676
4504
|
}
|
|
3677
4505
|
const tools = await listManagedTools({
|
|
@@ -3682,12 +4510,27 @@ Usage:
|
|
|
3682
4510
|
}),
|
|
3683
4511
|
});
|
|
3684
4512
|
if (!tools.length) {
|
|
3685
|
-
console.log(
|
|
4513
|
+
console.log(
|
|
4514
|
+
renderPage({
|
|
4515
|
+
title: "fclt managed",
|
|
4516
|
+
subtitle: "No managed tools.",
|
|
4517
|
+
sections: [],
|
|
4518
|
+
})
|
|
4519
|
+
);
|
|
3686
4520
|
return;
|
|
3687
4521
|
}
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
4522
|
+
console.log(
|
|
4523
|
+
renderPage({
|
|
4524
|
+
title: "fclt managed",
|
|
4525
|
+
subtitle: `${tools.length} managed tool${tools.length === 1 ? "" : "s"}`,
|
|
4526
|
+
sections: [
|
|
4527
|
+
{
|
|
4528
|
+
title: "Tools",
|
|
4529
|
+
lines: renderBullets(tools),
|
|
4530
|
+
},
|
|
4531
|
+
],
|
|
4532
|
+
})
|
|
4533
|
+
);
|
|
3691
4534
|
}
|
|
3692
4535
|
|
|
3693
4536
|
export async function syncCommand(argv: string[]) {
|