docks-kit 0.16.3 → 0.16.5
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/AGENTS.md +1 -1
- package/cli/src/argv.ts +2 -1
- package/cli/src/commands/model.ts +2 -1
- package/cli/src/commands/models.ts +2 -1
- package/cli/src/commands/status.ts +2 -11
- package/cli/src/commands/update.ts +2 -2
- package/cli/src/efforts.ts +0 -4
- package/cli/src/engine-native/bun.ts +1 -1
- package/cli/src/engine-native/claudePlugins.ts +390 -0
- package/cli/src/engine-native/claudeSync.ts +2 -387
- package/cli/src/engine-native/codexSync.ts +6 -146
- package/cli/src/engine-native/codexToml.ts +144 -3
- package/cli/src/engine-native/exec.ts +1 -5
- package/cli/src/engine-native/jq.ts +7 -0
- package/cli/src/engine-native/models.ts +31 -9
- package/cli/src/engine-native/ompSync.ts +3 -3
- package/cli/src/engine-native/os/darwin.ts +8 -56
- package/cli/src/engine-native/os/linux.ts +8 -56
- package/cli/src/engine-native/os/posix.ts +67 -0
- package/cli/src/engine-native/os/targets.ts +1 -1
- package/cli/src/engine-native/skillsSync.ts +3 -3
- package/cli/src/engine-native/toolchain.ts +7 -7
- package/cli/src/generated/sotPayload.ts +3 -3
- package/cli/src/manifests.ts +1 -18
- package/docks-kit +1 -1
- package/docks-kit.ps1 +1 -1
- package/package.json +1 -1
|
@@ -22,14 +22,14 @@ import {
|
|
|
22
22
|
syncClaudeModel
|
|
23
23
|
} from "./claudeSettingsModifiers"
|
|
24
24
|
import { claudeRuntimePaths, materializeClaudeSettings, type ClaudeRuntimePaths } from "./claudeRuntime"
|
|
25
|
+
import { syncLspServers, syncOptionalPlugins, syncPlugins } from "./claudePlugins"
|
|
25
26
|
import { RETIRED_PERMISSION_RULES } from "./claudeRetired"
|
|
26
27
|
import { p, spawnProcess, writeBytesIfChanged, writeTextIfChanged } from "./exec"
|
|
27
28
|
import type { Ctx } from "./index"
|
|
28
|
-
import {
|
|
29
|
+
import { deepMerge, isObject, jqStringify, parseJson, readJsonFile, type Json } from "./jq"
|
|
29
30
|
import { ExitError } from "./parseArgs"
|
|
30
31
|
import { hostOs } from "./os"
|
|
31
32
|
import { mergeSettings, reconcileSettings } from "./settings"
|
|
32
|
-
import { field } from "./toolchain"
|
|
33
33
|
import { payloadBytes, payloadDisplayPath, payloadText } from "../payload"
|
|
34
34
|
|
|
35
35
|
export type ClaudeRuntimeState =
|
|
@@ -556,391 +556,6 @@ function syncRemovals(ctx: Ctx, claudeDir: string, runtime: ClaudeRuntimeState):
|
|
|
556
556
|
}
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
-
// -------------------------------------------------------------- plugins ----
|
|
560
|
-
|
|
561
|
-
async function cli(args: Array<string>): Promise<{ ok: boolean; out: string }> {
|
|
562
|
-
const res = await spawnProcess("claude", args, { stdio: ["ignore", "pipe", "pipe"] })
|
|
563
|
-
return { ok: res.error === undefined && res.exitCode === 0, out: `${res.stdout}${res.stderr}` }
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function readJsonFile(file: string): Json | undefined {
|
|
567
|
-
return existsSync(file) ? parseJson(readFileSync(file, "utf8")) : undefined
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
function sortedKeys(obj: Json | undefined): Array<string> {
|
|
571
|
-
return obj !== undefined && isObject(obj) ? Object.keys(obj).sort(compareCodepoints) : []
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
/** claude::_plugin_user_scope_installed. */
|
|
575
|
-
export function pluginUserScopeInstalled(installedPlugins: string, pluginId: string): boolean {
|
|
576
|
-
const doc = readJsonFile(installedPlugins)
|
|
577
|
-
if (doc === undefined || !isObject(doc) || !isObject(doc["plugins"])) return false
|
|
578
|
-
const rec = (doc["plugins"] as { [k: string]: Json })[pluginId]
|
|
579
|
-
if (rec === undefined || rec === null) return false
|
|
580
|
-
const records = Array.isArray(rec) ? rec : [rec]
|
|
581
|
-
return records.some((r) => isObject(r) && r["scope"] === "user")
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
function nonUserScopeMarketplaces(installedDoc: Json | undefined): Set<string> {
|
|
585
|
-
const marketplaces = new Set<string>()
|
|
586
|
-
if (installedDoc === undefined || !isObject(installedDoc) || !isObject(installedDoc["plugins"])) {
|
|
587
|
-
return marketplaces
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
for (const [pluginId, value] of Object.entries(installedDoc["plugins"])) {
|
|
591
|
-
const records = Array.isArray(value) ? value : [value]
|
|
592
|
-
const hasNonUserInstall = records.some(
|
|
593
|
-
(record) => isObject(record) && (record["scope"] === "project" || record["scope"] === "local")
|
|
594
|
-
)
|
|
595
|
-
if (!hasNonUserInstall) continue
|
|
596
|
-
const separator = pluginId.lastIndexOf("@")
|
|
597
|
-
if (separator > 0) marketplaces.add(pluginId.slice(separator + 1))
|
|
598
|
-
}
|
|
599
|
-
return marketplaces
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
|
|
603
|
-
const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
|
|
604
|
-
const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json")
|
|
605
|
-
const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
|
|
606
|
-
|
|
607
|
-
if (ctx.dryRun) {
|
|
608
|
-
echo(
|
|
609
|
-
ctx.skipPluginRefresh
|
|
610
|
-
? "[dry-run] bootstrap + install missing plugins from SoT; skip refresh-only plugin updates"
|
|
611
|
-
: "[dry-run] bootstrap + update plugin marketplaces + plugins from SoT"
|
|
612
|
-
)
|
|
613
|
-
if (ctx.prune) {
|
|
614
|
-
echo("[dry-run] (--prune) would also uninstall plugins not in SoT and remove extra marketplaces")
|
|
615
|
-
}
|
|
616
|
-
return
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
if (ctx.services.deps.probe("claude").state === "missing") {
|
|
620
|
-
warn("claude CLI not in PATH — skipping plugin reconcile (run /plugin marketplace add + /plugin install manually)")
|
|
621
|
-
return
|
|
622
|
-
}
|
|
623
|
-
if (ctx.services.deps.probe("git").state === "missing") {
|
|
624
|
-
ctx.services.deps.warnMissing(
|
|
625
|
-
"git",
|
|
626
|
-
ctx.services.logger,
|
|
627
|
-
"plugin marketplaces are git repos — Claude plugin passes skipped; re-run sync after installing"
|
|
628
|
-
)
|
|
629
|
-
return
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
const repoSettings = parseJson(payloadText("SoT/.claude/settings.json"))
|
|
633
|
-
const repoObj = repoSettings !== undefined && isObject(repoSettings) ? repoSettings : {}
|
|
634
|
-
const sotMarketplaces = isObject(repoObj["extraKnownMarketplaces"]) ? repoObj["extraKnownMarketplaces"] : {}
|
|
635
|
-
const sotPlugins = isObject(repoObj["enabledPlugins"]) ? repoObj["enabledPlugins"] : {}
|
|
636
|
-
|
|
637
|
-
// Pass 1 — add missing marketplaces (SoT insertion order, like to_entries).
|
|
638
|
-
let addedMp = 0
|
|
639
|
-
let f1 = 0
|
|
640
|
-
for (const [mpName, mpValue] of Object.entries(sotMarketplaces)) {
|
|
641
|
-
const known = readJsonFile(knownMarketplaces)
|
|
642
|
-
if (known !== undefined && isObject(known) && known[mpName] !== undefined && known[mpName] !== null && known[mpName] !== false) continue
|
|
643
|
-
const repo = isObject(mpValue) && isObject(mpValue["source"]) ? String((mpValue["source"] as { [k: string]: Json })["repo"] ?? "") : ""
|
|
644
|
-
progress(`Adding marketplace ${mpName}...`)
|
|
645
|
-
const marketplaceResult = await cli(["plugin", "marketplace", "add", repo])
|
|
646
|
-
clearProgress()
|
|
647
|
-
if (marketplaceResult.ok) {
|
|
648
|
-
addedMp++
|
|
649
|
-
} else {
|
|
650
|
-
warn(`Failed to add marketplace: ${mpName} (${repo})`)
|
|
651
|
-
f1++
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
// Pass 2 — install SoT-enabled plugins missing at user scope (jq keys[] sorts),
|
|
656
|
-
// refreshing each source marketplace once so the install resolves a current snapshot.
|
|
657
|
-
let addedPl = 0
|
|
658
|
-
let f2 = 0
|
|
659
|
-
const refreshedMarketplaces = new Set<string>()
|
|
660
|
-
for (const pluginId of sortedKeys(sotPlugins)) {
|
|
661
|
-
if (pluginUserScopeInstalled(installedPlugins, pluginId)) continue
|
|
662
|
-
const separator = pluginId.lastIndexOf("@")
|
|
663
|
-
const mpName = separator > 0 ? pluginId.slice(separator + 1) : ""
|
|
664
|
-
if (mpName !== "" && !refreshedMarketplaces.has(mpName)) {
|
|
665
|
-
progress(`Refreshing marketplace ${mpName}...`)
|
|
666
|
-
await cli(["plugin", "marketplace", "update", mpName])
|
|
667
|
-
clearProgress()
|
|
668
|
-
refreshedMarketplaces.add(mpName)
|
|
669
|
-
}
|
|
670
|
-
progress(`Installing plugin ${pluginId}...`)
|
|
671
|
-
const installResult = await cli(["plugin", "install", pluginId])
|
|
672
|
-
clearProgress()
|
|
673
|
-
if (installResult.ok) {
|
|
674
|
-
addedPl++
|
|
675
|
-
} else {
|
|
676
|
-
warn(`Failed to install plugin: ${pluginId}`)
|
|
677
|
-
f2++
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
let updatedPl = 0
|
|
682
|
-
const installedDoc = readJsonFile(installedPlugins)
|
|
683
|
-
const installedKeys = installedDoc !== undefined && isObject(installedDoc) ? sortedKeys(installedDoc["plugins"]) : []
|
|
684
|
-
const nonUserMarketplaces = nonUserScopeMarketplaces(installedDoc)
|
|
685
|
-
// Kit-owned plugin IDs: SoT-declared plus this run's --claude-plugin opt-ins.
|
|
686
|
-
const kitPluginIds = new Set<string>(Object.keys(sotPlugins))
|
|
687
|
-
if (ctx.claudePlugins.includes("supabase")) kitPluginIds.add("supabase@claude-plugins-official")
|
|
688
|
-
if (ctx.claudePlugins.includes("n8n")) kitPluginIds.add("n8n-mcp-skills@n8n-mcp-skills")
|
|
689
|
-
|
|
690
|
-
// Kit-owned marketplaces: SoT-declared plus every marketplace those plugins come from.
|
|
691
|
-
const kitMarketplaces = new Set<string>(Object.keys(sotMarketplaces))
|
|
692
|
-
for (const pluginId of kitPluginIds) {
|
|
693
|
-
const separator = pluginId.lastIndexOf("@")
|
|
694
|
-
if (separator > 0) kitMarketplaces.add(pluginId.slice(separator + 1))
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
// Pass 3 — refresh the kit-owned marketplaces unless the update command
|
|
698
|
-
// selected its install-missing-only fast path.
|
|
699
|
-
if (!ctx.skipPluginRefresh) {
|
|
700
|
-
for (const mpName of [...kitMarketplaces].sort(compareCodepoints)) {
|
|
701
|
-
progress(`Refreshing marketplace ${mpName}...`)
|
|
702
|
-
await cli(["plugin", "marketplace", "update", mpName])
|
|
703
|
-
clearProgress()
|
|
704
|
-
}
|
|
705
|
-
// Pass 4 — update the kit-owned installed plugins.
|
|
706
|
-
for (const pluginId of [...kitPluginIds].sort(compareCodepoints)) {
|
|
707
|
-
if (!pluginUserScopeInstalled(installedPlugins, pluginId)) continue
|
|
708
|
-
progress(`Updating plugin ${pluginId}...`)
|
|
709
|
-
const updateResult = await cli(["plugin", "update", pluginId, "--scope", "user"])
|
|
710
|
-
clearProgress()
|
|
711
|
-
if (updateResult.out.includes("Successfully updated")) updatedPl++
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
// Pass 5 — prune-gated user-scope plugin uninstall.
|
|
716
|
-
let removedPl = 0
|
|
717
|
-
let removedMp = 0
|
|
718
|
-
let f5 = 0
|
|
719
|
-
let f6 = 0
|
|
720
|
-
if (ctx.prune) {
|
|
721
|
-
for (const pluginId of installedKeys) {
|
|
722
|
-
if (kitPluginIds.has(pluginId)) continue
|
|
723
|
-
if (!pluginUserScopeInstalled(installedPlugins, pluginId)) continue
|
|
724
|
-
progress(`Uninstalling plugin ${pluginId}...`)
|
|
725
|
-
const uninstallResult = await cli(["plugin", "uninstall", "-y", "--scope", "user", pluginId])
|
|
726
|
-
clearProgress()
|
|
727
|
-
if (uninstallResult.ok) {
|
|
728
|
-
removedPl++
|
|
729
|
-
} else {
|
|
730
|
-
warn(`Failed to uninstall plugin: ${pluginId}`)
|
|
731
|
-
f5++
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
// Pass 6 — prune-gated marketplace removal.
|
|
735
|
-
const known = readJsonFile(knownMarketplaces)
|
|
736
|
-
for (const mpName of sortedKeys(known)) {
|
|
737
|
-
if (mpName === "claude-plugins-official") continue
|
|
738
|
-
if (nonUserMarketplaces.has(mpName)) continue
|
|
739
|
-
if (kitMarketplaces.has(mpName)) continue
|
|
740
|
-
progress(`Removing marketplace ${mpName}...`)
|
|
741
|
-
const removeResult = await cli(["plugin", "marketplace", "remove", mpName])
|
|
742
|
-
clearProgress()
|
|
743
|
-
if (removeResult.ok) {
|
|
744
|
-
removedMp++
|
|
745
|
-
} else {
|
|
746
|
-
warn(`Failed to remove marketplace: ${mpName}`)
|
|
747
|
-
f6++
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
// Pass 7 — re-assert SoT enabled-state in the user settings.
|
|
753
|
-
if (await reassertEnabledState(ctx, repoObj, p(claudeDir, "settings.json"))) {
|
|
754
|
-
change("Plugin enable-state re-asserted from SoT in settings.json")
|
|
755
|
-
ctx.nextStepTriggers.claudePlugins = true
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
const failed = f1 + f2 + f5 + f6
|
|
759
|
-
if (addedMp > 0 || addedPl > 0 || updatedPl > 0 || removedPl > 0 || removedMp > 0) {
|
|
760
|
-
change(`Plugins synced (marketplaces: +${addedMp} -${removedMp}, plugins: +${addedPl} ~${updatedPl} -${removedPl})`)
|
|
761
|
-
ctx.nextStepTriggers.claudePlugins = true
|
|
762
|
-
} else {
|
|
763
|
-
verbose("Plugins already in sync")
|
|
764
|
-
}
|
|
765
|
-
if (failed > 0) {
|
|
766
|
-
warn(`${failed} plugin operation(s) failed — re-run sync or install manually`)
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
async function reassertEnabledState(ctx: Ctx, repoObj: { [k: string]: Json }, userSettingsFile: string): Promise<boolean> {
|
|
771
|
-
const { warn } = ctx.services.logger
|
|
772
|
-
if (!existsSync(userSettingsFile)) return false
|
|
773
|
-
const sotPlugins = isObject(repoObj["enabledPlugins"]) ? repoObj["enabledPlugins"] : {}
|
|
774
|
-
|
|
775
|
-
let cliDisabled = false
|
|
776
|
-
for (const [pluginId, value] of Object.entries(sotPlugins)) {
|
|
777
|
-
if (value !== false) continue
|
|
778
|
-
const user = readJsonFile(userSettingsFile)
|
|
779
|
-
const enabled = user !== undefined && isObject(user) && isObject(user["enabledPlugins"]) ? (user["enabledPlugins"] as { [k: string]: Json })[pluginId] : undefined
|
|
780
|
-
if (enabled !== true) continue
|
|
781
|
-
if ((await cli(["plugin", "disable", pluginId])).ok) {
|
|
782
|
-
cliDisabled = true
|
|
783
|
-
} else {
|
|
784
|
-
warn(`Failed to disable SoT-false plugin: ${pluginId} (will retry next sync)`)
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
const user = readJsonFile(userSettingsFile)
|
|
789
|
-
if (user === undefined || !isObject(user)) {
|
|
790
|
-
warn("enabledPlugins re-assert failed — false-keyed plugins may be left enabled")
|
|
791
|
-
return false
|
|
792
|
-
}
|
|
793
|
-
const beforeCanonical = jqStringify(user)
|
|
794
|
-
user["enabledPlugins"] = deepMerge(isObject(user["enabledPlugins"]) ? user["enabledPlugins"] : {}, sotPlugins)
|
|
795
|
-
const out = jqStringify(user)
|
|
796
|
-
if (out === beforeCanonical) return cliDisabled
|
|
797
|
-
writeFileSync(`${userSettingsFile}.tmp`, out)
|
|
798
|
-
renameSync(`${userSettingsFile}.tmp`, userSettingsFile)
|
|
799
|
-
return true
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
// ------------------------------------------------------ optional plugins ----
|
|
803
|
-
|
|
804
|
-
async function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, marketplaceRepo: string): Promise<boolean> {
|
|
805
|
-
const { change, clearProgress, progress, verbose, warn } = ctx.services.logger
|
|
806
|
-
const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
|
|
807
|
-
const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json")
|
|
808
|
-
const mpName = pluginId.slice(pluginId.lastIndexOf("@") + 1)
|
|
809
|
-
|
|
810
|
-
let marketplaceAdded = false
|
|
811
|
-
if (marketplaceRepo !== "") {
|
|
812
|
-
const known = readJsonFile(knownMarketplaces)
|
|
813
|
-
const has = known !== undefined && isObject(known) && known[mpName] !== undefined && known[mpName] !== null && known[mpName] !== false
|
|
814
|
-
if (!has) {
|
|
815
|
-
if (!(await cli(["plugin", "marketplace", "add", marketplaceRepo])).ok) {
|
|
816
|
-
warn(`Failed to add marketplace ${marketplaceRepo} for ${pluginId}`)
|
|
817
|
-
return false
|
|
818
|
-
}
|
|
819
|
-
marketplaceAdded = true
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
const wasInstalled = pluginUserScopeInstalled(installedPlugins, pluginId)
|
|
824
|
-
if (!wasInstalled) {
|
|
825
|
-
progress(`Installing plugin ${pluginId}...`)
|
|
826
|
-
const installResult = await cli(["plugin", "install", pluginId])
|
|
827
|
-
clearProgress()
|
|
828
|
-
if (!installResult.ok) {
|
|
829
|
-
if (marketplaceAdded) change(`Optional plugin ${pluginId}: marketplace added (install failed — will retry next sync)`)
|
|
830
|
-
warn(`Failed to install optional plugin ${pluginId}`)
|
|
831
|
-
return marketplaceAdded
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
const settingsDoc = readJsonFile(p(claudeDir, "settings.json"))
|
|
836
|
-
const wasEnabled =
|
|
837
|
-
settingsDoc !== undefined && isObject(settingsDoc) && isObject(settingsDoc["enabledPlugins"])
|
|
838
|
-
? (settingsDoc["enabledPlugins"] as { [k: string]: Json })[pluginId] === true
|
|
839
|
-
: false
|
|
840
|
-
|
|
841
|
-
if (!(await cli(["plugin", "enable", pluginId])).ok) {
|
|
842
|
-
if (marketplaceAdded || !wasInstalled) change(`Optional plugin ${pluginId}: installed (enable failed — will retry next sync)`)
|
|
843
|
-
warn(`Failed to enable optional plugin ${pluginId}`)
|
|
844
|
-
return marketplaceAdded || !wasInstalled
|
|
845
|
-
}
|
|
846
|
-
const changed = marketplaceAdded || !wasInstalled || !wasEnabled
|
|
847
|
-
if (changed) change(`Optional plugin opted in: ${pluginId}`)
|
|
848
|
-
else verbose(`Optional plugin already opted in: ${pluginId} (enable re-asserted)`)
|
|
849
|
-
return changed
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
async function syncOptionalPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
|
|
853
|
-
const { echo, warn } = ctx.services.logger
|
|
854
|
-
if (ctx.claudePlugins.length === 0) return
|
|
855
|
-
|
|
856
|
-
if (ctx.dryRun) {
|
|
857
|
-
if (ctx.claudePlugins.includes("supabase")) {
|
|
858
|
-
echo("[dry-run] (--claude-plugin=supabase) install + enable supabase@claude-plugins-official in deployed settings")
|
|
859
|
-
}
|
|
860
|
-
if (ctx.claudePlugins.includes("n8n")) {
|
|
861
|
-
echo("[dry-run] (--claude-plugin=n8n) add czlonkowski/n8n-skills marketplace + install + enable n8n-mcp-skills@n8n-mcp-skills")
|
|
862
|
-
}
|
|
863
|
-
return
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
if (ctx.services.deps.probe("claude").state === "missing") {
|
|
867
|
-
warn("claude CLI not in PATH — cannot opt in optional plugins (--claude-plugin)")
|
|
868
|
-
return
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
if (ctx.claudePlugins.includes("supabase")) {
|
|
872
|
-
if (await enableOptionalPlugin(ctx, claudeDir, "supabase@claude-plugins-official", "")) ctx.nextStepTriggers.claudePlugins = true
|
|
873
|
-
}
|
|
874
|
-
if (ctx.claudePlugins.includes("n8n")) {
|
|
875
|
-
if (await enableOptionalPlugin(ctx, claudeDir, "n8n-mcp-skills@n8n-mcp-skills", "czlonkowski/n8n-skills")) ctx.nextStepTriggers.claudePlugins = true
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
// ---------------------------------------------------------- LSP servers ----
|
|
880
|
-
|
|
881
|
-
function lspPkg(ctx: Ctx, tool: string, pkg: string): string | undefined {
|
|
882
|
-
const version = field(ctx, tool, "verified")
|
|
883
|
-
if (version !== "") return `${pkg}@${version}`
|
|
884
|
-
ctx.services.logger.warn(`Skipping ${pkg} install: ${tool} has no verified version in SoT/toolchain.json`)
|
|
885
|
-
return undefined
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
async function syncLspServers(ctx: Ctx): Promise<void> {
|
|
889
|
-
const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
|
|
890
|
-
const sot = parseJson(payloadText("SoT/.claude/settings.json"))
|
|
891
|
-
const enabled = sot !== undefined && isObject(sot) && isObject(sot["enabledPlugins"]) ? sot["enabledPlugins"] : undefined
|
|
892
|
-
if (enabled === undefined) return
|
|
893
|
-
const hasPhp = Object.prototype.hasOwnProperty.call(enabled, "php-lsp@claude-plugins-official")
|
|
894
|
-
const hasTs = Object.prototype.hasOwnProperty.call(enabled, "typescript-lsp@claude-plugins-official")
|
|
895
|
-
if (!hasPhp && !hasTs) return
|
|
896
|
-
|
|
897
|
-
const phpMissing = hasPhp && ctx.services.deps.probe("intelephense").state === "missing"
|
|
898
|
-
const tsServerMissing = hasTs && ctx.services.deps.probe("typescript-language-server").state === "missing"
|
|
899
|
-
const tscMissing = hasTs && ctx.services.deps.probe("tsc").state === "missing"
|
|
900
|
-
const missingToolCount = Number(phpMissing) + Number(tsServerMissing) + Number(tscMissing)
|
|
901
|
-
const missing = [
|
|
902
|
-
phpMissing ? lspPkg(ctx, "intelephense", "intelephense") : undefined,
|
|
903
|
-
tsServerMissing ? lspPkg(ctx, "typescript-language-server", "typescript-language-server") : undefined,
|
|
904
|
-
tscMissing ? lspPkg(ctx, "tsc", "typescript") : undefined
|
|
905
|
-
].filter((spec): spec is string => spec !== undefined)
|
|
906
|
-
|
|
907
|
-
if (missingToolCount === 0) {
|
|
908
|
-
if (ctx.dryRun) {
|
|
909
|
-
echo("[dry-run] LSP server binaries present")
|
|
910
|
-
} else {
|
|
911
|
-
verbose("LSP server binaries present")
|
|
912
|
-
}
|
|
913
|
-
return
|
|
914
|
-
}
|
|
915
|
-
if (missing.length === 0) return
|
|
916
|
-
|
|
917
|
-
const specs = missing.join(" ")
|
|
918
|
-
if (ctx.dryRun) {
|
|
919
|
-
echo(`[dry-run] would install: npm install -g ${specs}`)
|
|
920
|
-
return
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
if (ctx.services.deps.probe("npm").state === "missing") {
|
|
924
|
-
ctx.services.deps.warnMissing(
|
|
925
|
-
"npm",
|
|
926
|
-
ctx.services.logger,
|
|
927
|
-
`cannot install LSP servers (${specs}); the php-lsp/typescript-lsp plugins stay no-ops`
|
|
928
|
-
)
|
|
929
|
-
return
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
verbose(`Installing LSP servers via npm: ${specs}...`)
|
|
933
|
-
progress(`Installing LSP servers via npm: ${specs}...`)
|
|
934
|
-
const installResult = await spawnProcess("npm", ["install", "-g", ...missing], { stdio: "ignore" })
|
|
935
|
-
clearProgress()
|
|
936
|
-
if (installResult.exitCode === 0) {
|
|
937
|
-
change(`LSP servers installed (${specs})`)
|
|
938
|
-
ctx.nextStepTriggers.claudeRestart = true
|
|
939
|
-
} else {
|
|
940
|
-
warn(`npm install -g ${specs} failed. Try manually: npm install -g ${specs}`)
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
|
|
944
559
|
// -------------------------------------------------------------- summary ----
|
|
945
560
|
|
|
946
561
|
export function claudeSummary(ctx: Ctx, runtime: ClaudeRuntimeState): void {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"
|
|
7
7
|
|
|
8
|
-
import { syncCodexEffort, syncCodexModel
|
|
8
|
+
import { mergeTableSettings, mergeTopLevelSettings, syncCodexEffort, syncCodexModel } from "./codexToml"
|
|
9
9
|
import { p, spawnProcess } from "./exec"
|
|
10
10
|
import type { Ctx } from "./index"
|
|
11
11
|
import { compareCodepoints, isObject, jqStringify, parseJson, type Json } from "./jq"
|
|
@@ -217,7 +217,7 @@ function syncConfig(ctx: Ctx, sotConfigText: string, userConfig: string): void {
|
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
/** codex::scrub_deprecated_features — the [features].use_legacy_landlock awk pass. */
|
|
220
|
-
|
|
220
|
+
function scrubDeprecatedFeaturesText(content: string): string {
|
|
221
221
|
const lines = content.split("\n")
|
|
222
222
|
if (lines[lines.length - 1] === "") lines.pop()
|
|
223
223
|
let out = ""
|
|
@@ -317,146 +317,6 @@ function removeRetiredPluginTables(ctx: Ctx, userConfig: string): void {
|
|
|
317
317
|
for (const id of present) change(`Codex: removed retired plugin table [plugins."${id}"]`)
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
-
function mergeTopLevelSettings(sotConfigText: string, userConfig: string): void {
|
|
321
|
-
for (const line of sotConfigText.split("\n")) {
|
|
322
|
-
if (line.startsWith("[")) break
|
|
323
|
-
if (/^[ \t]*($|#)/.test(line)) continue
|
|
324
|
-
if (!/^[A-Za-z0-9_.-]+[ \t]*=/.test(line)) continue
|
|
325
|
-
const key = line.slice(0, line.indexOf("=")).replace(/[ \t]+$/, "")
|
|
326
|
-
replaceTopLevelSettingInFile(userConfig, key, line)
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
interface TomlTableHeader {
|
|
331
|
-
readonly path: string
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const TOML_BASIC_ESCAPES: Readonly<Record<string, string>> = {
|
|
335
|
-
b: "\b",
|
|
336
|
-
t: "\t",
|
|
337
|
-
n: "\n",
|
|
338
|
-
f: "\f",
|
|
339
|
-
r: "\r",
|
|
340
|
-
'"': '"',
|
|
341
|
-
"\\": "\\"
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function tomlBasicEscape(line: string, offset: number): { readonly next: number; readonly value: string } | undefined {
|
|
345
|
-
const escaped = line[offset]
|
|
346
|
-
const simple = escaped === undefined ? undefined : TOML_BASIC_ESCAPES[escaped]
|
|
347
|
-
if (simple !== undefined) return { next: offset + 1, value: simple }
|
|
348
|
-
const digits = escaped === "u" ? 4 : escaped === "U" ? 8 : 0
|
|
349
|
-
if (digits === 0) return undefined
|
|
350
|
-
const hex = line.slice(offset + 1, offset + 1 + digits)
|
|
351
|
-
if (hex.length !== digits || !/^[0-9A-Fa-f]+$/.test(hex)) return undefined
|
|
352
|
-
const codePoint = Number.parseInt(hex, 16)
|
|
353
|
-
if (codePoint > 0x10ffff || (codePoint >= 0xd800 && codePoint <= 0xdfff)) return undefined
|
|
354
|
-
return { next: offset + 1 + digits, value: String.fromCodePoint(codePoint) }
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/** Decode a table header to the TOML path that determines managed ownership. */
|
|
358
|
-
function tomlTableHeader(line: string): TomlTableHeader | undefined {
|
|
359
|
-
let offset = 0
|
|
360
|
-
const skipWhitespace = (): void => {
|
|
361
|
-
while (line[offset] === " " || line[offset] === "\t") offset++
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
skipWhitespace()
|
|
365
|
-
if (line[offset] !== "[") return undefined
|
|
366
|
-
offset++
|
|
367
|
-
const array = line[offset] === "["
|
|
368
|
-
if (array) offset++
|
|
369
|
-
|
|
370
|
-
const keys: Array<string> = []
|
|
371
|
-
while (true) {
|
|
372
|
-
skipWhitespace()
|
|
373
|
-
const quote = line[offset]
|
|
374
|
-
let key = ""
|
|
375
|
-
if (quote === '"' || quote === "'") {
|
|
376
|
-
offset++
|
|
377
|
-
let closed = false
|
|
378
|
-
while (offset < line.length) {
|
|
379
|
-
const char = line[offset]!
|
|
380
|
-
if (char === quote) {
|
|
381
|
-
offset++
|
|
382
|
-
closed = true
|
|
383
|
-
break
|
|
384
|
-
}
|
|
385
|
-
if (quote === '"' && char === "\\") {
|
|
386
|
-
const escape = tomlBasicEscape(line, offset + 1)
|
|
387
|
-
if (escape === undefined) return undefined
|
|
388
|
-
key += escape.value
|
|
389
|
-
offset = escape.next
|
|
390
|
-
continue
|
|
391
|
-
}
|
|
392
|
-
if (char === "\n" || char === "\r") return undefined
|
|
393
|
-
key += char
|
|
394
|
-
offset++
|
|
395
|
-
}
|
|
396
|
-
if (!closed) return undefined
|
|
397
|
-
} else {
|
|
398
|
-
const start = offset
|
|
399
|
-
while (offset < line.length && /[A-Za-z0-9_-]/.test(line[offset]!)) offset++
|
|
400
|
-
if (offset === start) return undefined
|
|
401
|
-
key = line.slice(start, offset)
|
|
402
|
-
}
|
|
403
|
-
keys.push(key)
|
|
404
|
-
|
|
405
|
-
skipWhitespace()
|
|
406
|
-
if (line[offset] === ".") {
|
|
407
|
-
offset++
|
|
408
|
-
continue
|
|
409
|
-
}
|
|
410
|
-
if (line[offset] !== "]") return undefined
|
|
411
|
-
offset++
|
|
412
|
-
if (array) {
|
|
413
|
-
if (line[offset] !== "]") return undefined
|
|
414
|
-
offset++
|
|
415
|
-
}
|
|
416
|
-
skipWhitespace()
|
|
417
|
-
if (offset < line.length && line[offset] !== "#") return undefined
|
|
418
|
-
return { path: JSON.stringify(keys) }
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function mergeTableSettingsText(sotConfigText: string, userConfigText: string): string {
|
|
423
|
-
const sotLines = sotConfigText.split("\n")
|
|
424
|
-
let merged = userConfigText
|
|
425
|
-
for (let tableOffset = 0; tableOffset < sotLines.length; tableOffset++) {
|
|
426
|
-
const managedHeader = tomlTableHeader(sotLines[tableOffset]!)
|
|
427
|
-
if (managedHeader === undefined) continue
|
|
428
|
-
|
|
429
|
-
const block: Array<string> = []
|
|
430
|
-
for (let blockOffset = tableOffset; blockOffset < sotLines.length; blockOffset++) {
|
|
431
|
-
const line = sotLines[blockOffset]!
|
|
432
|
-
if (blockOffset !== tableOffset && tomlTableHeader(line) !== undefined) break
|
|
433
|
-
block.push(line)
|
|
434
|
-
}
|
|
435
|
-
const tableBlock = block.join("\n").replace(/\n+$/, "")
|
|
436
|
-
|
|
437
|
-
const userLines = merged.split("\n")
|
|
438
|
-
if (userLines[userLines.length - 1] === "") userLines.pop()
|
|
439
|
-
let skip = false
|
|
440
|
-
const kept: Array<string> = []
|
|
441
|
-
for (const line of userLines) {
|
|
442
|
-
const header = tomlTableHeader(line)
|
|
443
|
-
if (header !== undefined) {
|
|
444
|
-
skip = header.path === managedHeader.path
|
|
445
|
-
if (skip) continue
|
|
446
|
-
}
|
|
447
|
-
if (!skip) kept.push(line)
|
|
448
|
-
}
|
|
449
|
-
merged = `${kept.join("\n")}\n\n${tableBlock}\n`
|
|
450
|
-
}
|
|
451
|
-
return merged
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function mergeTableSettings(sotConfigText: string, userConfig: string): void {
|
|
455
|
-
const next = mergeTableSettingsText(sotConfigText, readFileSync(userConfig, "utf8"))
|
|
456
|
-
writeFileSync(`${userConfig}.tmp`, next)
|
|
457
|
-
renameSync(`${userConfig}.tmp`, userConfig)
|
|
458
|
-
}
|
|
459
|
-
|
|
460
320
|
// ------------------------------------------------------- rules + agents ----
|
|
461
321
|
|
|
462
322
|
function syncRules(ctx: Ctx, sotRules: ReadonlyArray<PayloadPath>, userRulesDir: string): void {
|
|
@@ -560,7 +420,7 @@ function syncMarketplace(ctx: Ctx, sotMarketplaceText: string, userMarketplace:
|
|
|
560
420
|
* wins per plugin name; distinct names end up descending by name, exactly
|
|
561
421
|
* like jq's unique_by (ascending) followed by reverse.
|
|
562
422
|
*/
|
|
563
|
-
|
|
423
|
+
function mergeMarketplace(repo: Json, user: Json): Json {
|
|
564
424
|
const u = isObject(user) ? user : {}
|
|
565
425
|
const r = isObject(repo) ? repo : {}
|
|
566
426
|
const coalesce = (a: Json | undefined, b: Json | undefined): Json =>
|
|
@@ -586,7 +446,7 @@ export function mergeMarketplace(repo: Json, user: Json): Json {
|
|
|
586
446
|
// -------------------------------------------------------------- plugins ----
|
|
587
447
|
|
|
588
448
|
/** codex::_marketplace_source — first `source =` inside [marketplaces.<name>]. */
|
|
589
|
-
|
|
449
|
+
function marketplaceSource(marketplace: string, configFile: string): string {
|
|
590
450
|
if (!existsSync(configFile)) return ""
|
|
591
451
|
let inMarketplace = false
|
|
592
452
|
for (const line of readFileSync(configFile, "utf8").split("\n")) {
|
|
@@ -629,12 +489,12 @@ async function removeLegacyDocksMarketplace(ctx: Ctx, userConfig: string): Promi
|
|
|
629
489
|
const standaloneInstallCommand = (ctx: Ctx): string => ctx.services.deps.spec("codex").installHint()
|
|
630
490
|
|
|
631
491
|
/** codex::_enabled_plugin_ids — [plugins."<id>"] tables with enabled = true. */
|
|
632
|
-
|
|
492
|
+
function enabledPluginIds(configFile: string): Array<string> {
|
|
633
493
|
if (!existsSync(configFile)) return []
|
|
634
494
|
return enabledPluginIdsFromText(readFileSync(configFile, "utf8"))
|
|
635
495
|
}
|
|
636
496
|
|
|
637
|
-
|
|
497
|
+
function enabledPluginIdsFromText(configText: string): Array<string> {
|
|
638
498
|
const ids: Array<string> = []
|
|
639
499
|
let plugin = ""
|
|
640
500
|
let enabled = false
|