docks-kit 0.17.1 → 0.17.3
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 +41 -33
- package/cli/docs/omp-context.md +104 -0
- package/cli/docs/omp-models.md +106 -56
- package/cli/src/argv.ts +38 -469
- package/cli/src/argvSurface.ts +371 -0
- package/cli/src/argvValidate.ts +122 -0
- package/cli/src/commands/docs.ts +62 -42
- package/cli/src/commands/harnesses.ts +38 -45
- package/cli/src/commands/model.ts +48 -53
- package/cli/src/commands/models.ts +24 -26
- package/cli/src/commands/omp.ts +132 -112
- package/cli/src/commands/plugins.ts +20 -20
- package/cli/src/commands/skills.ts +22 -20
- package/cli/src/commands/status.ts +87 -80
- package/cli/src/commands/sync.ts +113 -87
- package/cli/src/commands/toolchain.ts +21 -21
- package/cli/src/commands/update.ts +139 -117
- package/cli/src/efforts.ts +35 -34
- package/cli/src/engine-native/DESIGN.md +30 -30
- package/cli/src/engine-native/bun.ts +77 -55
- package/cli/src/engine-native/claudeLsp.ts +154 -0
- package/cli/src/engine-native/claudeOptionalPlugins.ts +113 -0
- package/cli/src/engine-native/claudePluginPasses.ts +348 -0
- package/cli/src/engine-native/claudeRemovals.ts +195 -0
- package/cli/src/engine-native/claudeRetired.ts +4 -4
- package/cli/src/engine-native/claudeRuntime.ts +94 -68
- package/cli/src/engine-native/claudeSettings.ts +174 -0
- package/cli/src/engine-native/claudeSettingsModifiers.ts +50 -54
- package/cli/src/engine-native/claudeSync.ts +176 -456
- package/cli/src/engine-native/codexConfig.ts +158 -0
- package/cli/src/engine-native/codexHooks.ts +91 -0
- package/cli/src/engine-native/codexPlugins.ts +290 -0
- package/cli/src/engine-native/codexStatus.ts +24 -0
- package/cli/src/engine-native/codexSync.ts +110 -568
- package/cli/src/engine-native/codexToml.ts +140 -138
- package/cli/src/engine-native/deps.ts +144 -107
- package/cli/src/engine-native/engineCtx.ts +126 -0
- package/cli/src/engine-native/exec.ts +98 -87
- package/cli/src/engine-native/failures.ts +3 -3
- package/cli/src/engine-native/harnesses.ts +62 -72
- package/cli/src/engine-native/index.ts +34 -315
- package/cli/src/engine-native/jq.ts +25 -24
- package/cli/src/engine-native/logger.ts +98 -100
- package/cli/src/engine-native/models.ts +50 -45
- package/cli/src/engine-native/modes.ts +83 -80
- package/cli/src/engine-native/ompFileDeploy.ts +111 -0
- package/cli/src/engine-native/ompMarketplace.ts +136 -0
- package/cli/src/engine-native/ompOverlay.ts +67 -64
- package/cli/src/engine-native/ompPaths.ts +49 -38
- package/cli/src/engine-native/ompPlugins.ts +170 -0
- package/cli/src/engine-native/ompRemovals.ts +159 -0
- package/cli/src/engine-native/ompSync.ts +69 -409
- package/cli/src/engine-native/ompYaml.ts +43 -41
- package/cli/src/engine-native/os/darwin.ts +5 -5
- package/cli/src/engine-native/os/index.ts +15 -15
- package/cli/src/engine-native/os/linux.ts +5 -5
- package/cli/src/engine-native/os/posix.ts +28 -24
- package/cli/src/engine-native/os/targets.ts +17 -17
- package/cli/src/engine-native/os/types.ts +31 -31
- package/cli/src/engine-native/os/windows.ts +61 -61
- package/cli/src/engine-native/parseArgs.ts +122 -372
- package/cli/src/engine-native/parseHelp.ts +66 -0
- package/cli/src/engine-native/parseModifiers.ts +222 -0
- package/cli/src/engine-native/services.ts +34 -34
- package/cli/src/engine-native/settings.ts +15 -15
- package/cli/src/engine-native/sharedTypes.d.ts +46 -0
- package/cli/src/engine-native/skillsInstall.ts +105 -0
- package/cli/src/engine-native/skillsLinks.ts +203 -0
- package/cli/src/engine-native/skillsManifest.ts +16 -0
- package/cli/src/engine-native/skillsPrune.ts +108 -0
- package/cli/src/engine-native/skillsSync.ts +28 -362
- package/cli/src/engine-native/syncConcurrency.ts +55 -0
- package/cli/src/engine-native/syncDispatch.ts +141 -0
- package/cli/src/engine-native/toolchain.ts +65 -62
- package/cli/src/engine.ts +65 -57
- package/cli/src/generated/sotPayload.ts +7 -7
- package/cli/src/kitHome.ts +44 -40
- package/cli/src/main.ts +63 -56
- package/cli/src/manifests.ts +53 -54
- package/cli/src/md.d.ts +2 -2
- package/cli/src/payload.ts +9 -9
- package/cli/src/services.ts +21 -13
- package/cli/tsconfig.json +4 -1
- package/docks-kit +13 -2
- package/docks-kit.ps1 +21 -6
- package/package.json +10 -2
- package/cli/src/engine-native/claudePlugins.ts +0 -510
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EngineNative `sync claude` plugin install and refresh passes. Pass order
|
|
3
|
+
* inside `syncPlugins` is load-bearing and golden-tested; message strings
|
|
4
|
+
* and spawned argv are part of the contract.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, renameSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { p, spawnProcess } from "./exec";
|
|
8
|
+
import { recordFailure } from "./failures";
|
|
9
|
+
import type { Ctx } from "./index";
|
|
10
|
+
import {
|
|
11
|
+
compareCodepoints,
|
|
12
|
+
deepMerge,
|
|
13
|
+
isObject,
|
|
14
|
+
jqStringify,
|
|
15
|
+
parseJson,
|
|
16
|
+
readJsonFile,
|
|
17
|
+
type Json,
|
|
18
|
+
} from "./jq";
|
|
19
|
+
import { payloadText } from "../payload";
|
|
20
|
+
import type { JsonObject } from "./sharedTypes";
|
|
21
|
+
|
|
22
|
+
export async function cli(
|
|
23
|
+
args: Array<string>,
|
|
24
|
+
): Promise<{ ok: boolean; out: string; detail: string }> {
|
|
25
|
+
const res = await spawnProcess("claude", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
26
|
+
// A spawn error carries its cause in `error`, not in either stream, and that
|
|
27
|
+
// is the case a failure message cannot afford to drop: it names an
|
|
28
|
+
// unresolvable launcher instead of a rejected command.
|
|
29
|
+
const out = res.error !== undefined ? res.error.message : `${res.stdout}${res.stderr}`;
|
|
30
|
+
const detail =
|
|
31
|
+
out
|
|
32
|
+
.split("\n")
|
|
33
|
+
.map((line) => line.trim())
|
|
34
|
+
.find((line) => line !== "") ?? "unknown error";
|
|
35
|
+
return { ok: res.error === undefined && res.exitCode === 0, out, detail };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Failure message tail: the cause, then the command to re-run by hand. */
|
|
39
|
+
function manually(detail: string, args: Array<string>): string {
|
|
40
|
+
return `${detail}; run manually: claude ${args.join(" ")}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sortedKeys(obj: Json | undefined): Array<string> {
|
|
44
|
+
return obj !== undefined && isObject(obj) ? Object.keys(obj).sort(compareCodepoints) : [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** claude::_plugin_user_scope_installed. */
|
|
48
|
+
export function pluginUserScopeInstalled(installedPlugins: string, pluginId: string): boolean {
|
|
49
|
+
const doc = readJsonFile(installedPlugins);
|
|
50
|
+
if (doc === undefined || !isObject(doc) || !isObject(doc["plugins"])) return false;
|
|
51
|
+
const rec = (doc["plugins"] as JsonObject)[pluginId];
|
|
52
|
+
if (rec === undefined || rec === null) return false;
|
|
53
|
+
const records = Array.isArray(rec) ? rec : [rec];
|
|
54
|
+
return records.some((r) => isObject(r) && r["scope"] === "user");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function nonUserScopeMarketplaces(installedDoc: Json | undefined): Set<string> {
|
|
58
|
+
const marketplaces = new Set<string>();
|
|
59
|
+
if (installedDoc === undefined || !isObject(installedDoc) || !isObject(installedDoc["plugins"])) {
|
|
60
|
+
return marketplaces;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const [pluginId, value] of Object.entries(installedDoc["plugins"])) {
|
|
64
|
+
const records = Array.isArray(value) ? value : [value];
|
|
65
|
+
const hasNonUserInstall = records.some(
|
|
66
|
+
(record) =>
|
|
67
|
+
isObject(record) && (record["scope"] === "project" || record["scope"] === "local"),
|
|
68
|
+
);
|
|
69
|
+
if (!hasNonUserInstall) continue;
|
|
70
|
+
const separator = pluginId.lastIndexOf("@");
|
|
71
|
+
if (separator > 0) marketplaces.add(pluginId.slice(separator + 1));
|
|
72
|
+
}
|
|
73
|
+
return marketplaces;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
|
|
77
|
+
const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger;
|
|
78
|
+
const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json");
|
|
79
|
+
const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json");
|
|
80
|
+
|
|
81
|
+
if (ctx.dryRun) {
|
|
82
|
+
echo(
|
|
83
|
+
ctx.skipPluginRefresh
|
|
84
|
+
? "[dry-run] bootstrap + install missing plugins from SoT; skip refresh-only plugin updates"
|
|
85
|
+
: "[dry-run] bootstrap + update plugin marketplaces + plugins from SoT",
|
|
86
|
+
);
|
|
87
|
+
if (ctx.prune) {
|
|
88
|
+
echo(
|
|
89
|
+
"[dry-run] (--prune) would also uninstall plugins not in SoT and remove extra marketplaces",
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (ctx.services.deps.probe("claude").state === "missing") {
|
|
96
|
+
warn(
|
|
97
|
+
"claude CLI not in PATH — skipping plugin reconcile (run /plugin marketplace add + /plugin install manually)",
|
|
98
|
+
);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (ctx.services.deps.probe("git").state === "missing") {
|
|
102
|
+
ctx.services.deps.warnMissing(
|
|
103
|
+
"git",
|
|
104
|
+
ctx.services.logger,
|
|
105
|
+
"plugin marketplaces are git repos — Claude plugin passes skipped; re-run sync after installing",
|
|
106
|
+
);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const repoSettings = parseJson(payloadText("SoT/.claude/settings.json"));
|
|
111
|
+
const repoObj = repoSettings !== undefined && isObject(repoSettings) ? repoSettings : {};
|
|
112
|
+
const sotMarketplaces = isObject(repoObj["extraKnownMarketplaces"])
|
|
113
|
+
? repoObj["extraKnownMarketplaces"]
|
|
114
|
+
: {};
|
|
115
|
+
const sotPlugins = isObject(repoObj["enabledPlugins"]) ? repoObj["enabledPlugins"] : {};
|
|
116
|
+
|
|
117
|
+
// Pass 1 — add missing marketplaces (SoT insertion order, like to_entries).
|
|
118
|
+
let addedMp = 0;
|
|
119
|
+
let f1 = 0;
|
|
120
|
+
for (const [mpName, mpValue] of Object.entries(sotMarketplaces)) {
|
|
121
|
+
const known = readJsonFile(knownMarketplaces);
|
|
122
|
+
if (
|
|
123
|
+
known !== undefined &&
|
|
124
|
+
isObject(known) &&
|
|
125
|
+
known[mpName] !== undefined &&
|
|
126
|
+
known[mpName] !== null &&
|
|
127
|
+
known[mpName] !== false
|
|
128
|
+
)
|
|
129
|
+
continue;
|
|
130
|
+
const repo =
|
|
131
|
+
isObject(mpValue) && isObject(mpValue["source"])
|
|
132
|
+
? String((mpValue["source"] as JsonObject)["repo"] ?? "")
|
|
133
|
+
: "";
|
|
134
|
+
progress(`Adding marketplace ${mpName}...`);
|
|
135
|
+
const marketplaceResult = await cli(["plugin", "marketplace", "add", repo]);
|
|
136
|
+
clearProgress();
|
|
137
|
+
if (marketplaceResult.ok) {
|
|
138
|
+
addedMp++;
|
|
139
|
+
} else {
|
|
140
|
+
recordFailure(
|
|
141
|
+
ctx,
|
|
142
|
+
`Failed to add marketplace: ${mpName} (${repo}): ${manually(marketplaceResult.detail, ["plugin", "marketplace", "add", repo])}`,
|
|
143
|
+
);
|
|
144
|
+
f1++;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Pass 2 — install SoT-enabled plugins missing at user scope (jq keys[] sorts),
|
|
149
|
+
// refreshing each source marketplace once so the install resolves a current snapshot.
|
|
150
|
+
let addedPl = 0;
|
|
151
|
+
let f2 = 0;
|
|
152
|
+
let f3 = 0;
|
|
153
|
+
let f4 = 0;
|
|
154
|
+
const refreshedMarketplaces = new Set<string>();
|
|
155
|
+
for (const pluginId of sortedKeys(sotPlugins)) {
|
|
156
|
+
if (pluginUserScopeInstalled(installedPlugins, pluginId)) continue;
|
|
157
|
+
const separator = pluginId.lastIndexOf("@");
|
|
158
|
+
const mpName = separator > 0 ? pluginId.slice(separator + 1) : "";
|
|
159
|
+
if (mpName !== "" && !refreshedMarketplaces.has(mpName)) {
|
|
160
|
+
progress(`Refreshing marketplace ${mpName}...`);
|
|
161
|
+
const refreshResult = await cli(["plugin", "marketplace", "update", mpName]);
|
|
162
|
+
clearProgress();
|
|
163
|
+
if (!refreshResult.ok) {
|
|
164
|
+
recordFailure(
|
|
165
|
+
ctx,
|
|
166
|
+
`Failed to refresh marketplace: ${mpName}: ${manually(refreshResult.detail, ["plugin", "marketplace", "update", mpName])}`,
|
|
167
|
+
);
|
|
168
|
+
f3++;
|
|
169
|
+
}
|
|
170
|
+
refreshedMarketplaces.add(mpName);
|
|
171
|
+
}
|
|
172
|
+
progress(`Installing plugin ${pluginId}...`);
|
|
173
|
+
const installResult = await cli(["plugin", "install", pluginId]);
|
|
174
|
+
clearProgress();
|
|
175
|
+
if (installResult.ok) {
|
|
176
|
+
addedPl++;
|
|
177
|
+
} else {
|
|
178
|
+
recordFailure(
|
|
179
|
+
ctx,
|
|
180
|
+
`Failed to install plugin: ${pluginId}: ${manually(installResult.detail, ["plugin", "install", pluginId])}`,
|
|
181
|
+
);
|
|
182
|
+
f2++;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let updatedPl = 0;
|
|
187
|
+
const installedDoc = readJsonFile(installedPlugins);
|
|
188
|
+
const installedKeys =
|
|
189
|
+
installedDoc !== undefined && isObject(installedDoc) ? sortedKeys(installedDoc["plugins"]) : [];
|
|
190
|
+
const nonUserMarketplaces = nonUserScopeMarketplaces(installedDoc);
|
|
191
|
+
// Kit-owned plugin IDs: SoT-declared plus this run's --claude-plugin opt-ins.
|
|
192
|
+
const kitPluginIds = new Set<string>(Object.keys(sotPlugins));
|
|
193
|
+
if (ctx.claudePlugins.includes("supabase")) kitPluginIds.add("supabase@claude-plugins-official");
|
|
194
|
+
if (ctx.claudePlugins.includes("n8n")) kitPluginIds.add("n8n-mcp-skills@n8n-mcp-skills");
|
|
195
|
+
|
|
196
|
+
// Kit-owned marketplaces: SoT-declared plus every marketplace those plugins come from.
|
|
197
|
+
const kitMarketplaces = new Set<string>(Object.keys(sotMarketplaces));
|
|
198
|
+
for (const pluginId of kitPluginIds) {
|
|
199
|
+
const separator = pluginId.lastIndexOf("@");
|
|
200
|
+
if (separator > 0) kitMarketplaces.add(pluginId.slice(separator + 1));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Pass 3 — refresh the kit-owned marketplaces unless the caller passed
|
|
204
|
+
// --skip-plugin-refresh by hand. Pass 2 already refreshed the source
|
|
205
|
+
// marketplace of every plugin it installed, so skip those: a second
|
|
206
|
+
// fetch seconds later cannot resolve a newer snapshot, and re-running it
|
|
207
|
+
// would duplicate one failure in the ledger and in the failed-operation count.
|
|
208
|
+
if (!ctx.skipPluginRefresh) {
|
|
209
|
+
for (const mpName of [...kitMarketplaces].sort(compareCodepoints)) {
|
|
210
|
+
if (refreshedMarketplaces.has(mpName)) continue;
|
|
211
|
+
progress(`Refreshing marketplace ${mpName}...`);
|
|
212
|
+
const refreshResult = await cli(["plugin", "marketplace", "update", mpName]);
|
|
213
|
+
clearProgress();
|
|
214
|
+
refreshedMarketplaces.add(mpName);
|
|
215
|
+
if (!refreshResult.ok) {
|
|
216
|
+
recordFailure(
|
|
217
|
+
ctx,
|
|
218
|
+
`Failed to refresh marketplace: ${mpName}: ${manually(refreshResult.detail, ["plugin", "marketplace", "update", mpName])}`,
|
|
219
|
+
);
|
|
220
|
+
f3++;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Pass 4 — update the kit-owned installed plugins.
|
|
224
|
+
for (const pluginId of [...kitPluginIds].sort(compareCodepoints)) {
|
|
225
|
+
if (!pluginUserScopeInstalled(installedPlugins, pluginId)) continue;
|
|
226
|
+
progress(`Updating plugin ${pluginId}...`);
|
|
227
|
+
const updateResult = await cli(["plugin", "update", pluginId, "--scope", "user"]);
|
|
228
|
+
clearProgress();
|
|
229
|
+
if (!updateResult.ok) {
|
|
230
|
+
recordFailure(
|
|
231
|
+
ctx,
|
|
232
|
+
`Failed to update plugin: ${pluginId}: ${manually(updateResult.detail, ["plugin", "update", pluginId, "--scope", "user"])}`,
|
|
233
|
+
);
|
|
234
|
+
f4++;
|
|
235
|
+
} else if (updateResult.out.includes("Successfully updated")) {
|
|
236
|
+
updatedPl++;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Pass 5 — prune-gated user-scope plugin uninstall.
|
|
242
|
+
let removedPl = 0;
|
|
243
|
+
let removedMp = 0;
|
|
244
|
+
let f5 = 0;
|
|
245
|
+
let f6 = 0;
|
|
246
|
+
if (ctx.prune) {
|
|
247
|
+
for (const pluginId of installedKeys) {
|
|
248
|
+
if (kitPluginIds.has(pluginId)) continue;
|
|
249
|
+
if (!pluginUserScopeInstalled(installedPlugins, pluginId)) continue;
|
|
250
|
+
progress(`Uninstalling plugin ${pluginId}...`);
|
|
251
|
+
const uninstallResult = await cli(["plugin", "uninstall", "-y", "--scope", "user", pluginId]);
|
|
252
|
+
clearProgress();
|
|
253
|
+
if (uninstallResult.ok) {
|
|
254
|
+
removedPl++;
|
|
255
|
+
} else {
|
|
256
|
+
recordFailure(
|
|
257
|
+
ctx,
|
|
258
|
+
`Failed to uninstall plugin: ${pluginId}: ${manually(uninstallResult.detail, ["plugin", "uninstall", "-y", "--scope", "user", pluginId])}`,
|
|
259
|
+
);
|
|
260
|
+
f5++;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Pass 6 — prune-gated marketplace removal.
|
|
264
|
+
const known = readJsonFile(knownMarketplaces);
|
|
265
|
+
for (const mpName of sortedKeys(known)) {
|
|
266
|
+
if (mpName === "claude-plugins-official") continue;
|
|
267
|
+
if (nonUserMarketplaces.has(mpName)) continue;
|
|
268
|
+
if (kitMarketplaces.has(mpName)) continue;
|
|
269
|
+
progress(`Removing marketplace ${mpName}...`);
|
|
270
|
+
const removeResult = await cli(["plugin", "marketplace", "remove", mpName]);
|
|
271
|
+
clearProgress();
|
|
272
|
+
if (removeResult.ok) {
|
|
273
|
+
removedMp++;
|
|
274
|
+
} else {
|
|
275
|
+
recordFailure(
|
|
276
|
+
ctx,
|
|
277
|
+
`Failed to remove marketplace: ${mpName}: ${manually(removeResult.detail, ["plugin", "marketplace", "remove", mpName])}`,
|
|
278
|
+
);
|
|
279
|
+
f6++;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Pass 7 — re-assert SoT enabled-state in the user settings.
|
|
285
|
+
if (await reassertEnabledState(ctx, repoObj, p(claudeDir, "settings.json"))) {
|
|
286
|
+
change("Plugin enable-state re-asserted from SoT in settings.json");
|
|
287
|
+
ctx.nextStepTriggers.claudePlugins = true;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const failed = f1 + f2 + f3 + f4 + f5 + f6;
|
|
291
|
+
if (addedMp > 0 || addedPl > 0 || updatedPl > 0 || removedPl > 0 || removedMp > 0) {
|
|
292
|
+
change(
|
|
293
|
+
`Plugins synced (marketplaces: +${addedMp} -${removedMp}, plugins: +${addedPl} ~${updatedPl} -${removedPl})`,
|
|
294
|
+
);
|
|
295
|
+
ctx.nextStepTriggers.claudePlugins = true;
|
|
296
|
+
} else {
|
|
297
|
+
verbose("Plugins already in sync");
|
|
298
|
+
}
|
|
299
|
+
if (failed > 0) {
|
|
300
|
+
warn(`${failed} plugin operation(s) failed — re-run sync or install manually`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function reassertEnabledState(
|
|
305
|
+
ctx: Ctx,
|
|
306
|
+
repoObj: JsonObject,
|
|
307
|
+
userSettingsFile: string,
|
|
308
|
+
): Promise<boolean> {
|
|
309
|
+
const { warn } = ctx.services.logger;
|
|
310
|
+
if (!existsSync(userSettingsFile)) return false;
|
|
311
|
+
const sotPlugins = isObject(repoObj["enabledPlugins"]) ? repoObj["enabledPlugins"] : {};
|
|
312
|
+
|
|
313
|
+
let cliDisabled = false;
|
|
314
|
+
for (const [pluginId, value] of Object.entries(sotPlugins)) {
|
|
315
|
+
if (value !== false) continue;
|
|
316
|
+
const user = readJsonFile(userSettingsFile);
|
|
317
|
+
const enabled =
|
|
318
|
+
user !== undefined && isObject(user) && isObject(user["enabledPlugins"])
|
|
319
|
+
? (user["enabledPlugins"] as JsonObject)[pluginId]
|
|
320
|
+
: undefined;
|
|
321
|
+
if (enabled !== true) continue;
|
|
322
|
+
const disableResult = await cli(["plugin", "disable", pluginId]);
|
|
323
|
+
if (disableResult.ok) {
|
|
324
|
+
cliDisabled = true;
|
|
325
|
+
} else {
|
|
326
|
+
recordFailure(
|
|
327
|
+
ctx,
|
|
328
|
+
`Failed to disable SoT-false plugin: ${pluginId} (will retry next sync): ${manually(disableResult.detail, ["plugin", "disable", pluginId])}`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const user = readJsonFile(userSettingsFile);
|
|
334
|
+
if (user === undefined || !isObject(user)) {
|
|
335
|
+
warn("enabledPlugins re-assert failed — false-keyed plugins may be left enabled");
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
const beforeCanonical = jqStringify(user);
|
|
339
|
+
user["enabledPlugins"] = deepMerge(
|
|
340
|
+
isObject(user["enabledPlugins"]) ? user["enabledPlugins"] : {},
|
|
341
|
+
sotPlugins,
|
|
342
|
+
);
|
|
343
|
+
const out = jqStringify(user);
|
|
344
|
+
if (out === beforeCanonical) return cliDisabled;
|
|
345
|
+
writeFileSync(`${userSettingsFile}.tmp`, out);
|
|
346
|
+
renameSync(`${userSettingsFile}.tmp`, userSettingsFile);
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EngineNative `sync claude` retired-artifact pruning. The SoT no longer
|
|
3
|
+
* ships the inventoried rules and artifacts, so this pass force-prunes them
|
|
4
|
+
* from existing installations without `--reconcile`. Message strings and
|
|
5
|
+
* prune semantics are part of the contract.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync, renameSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import type { ClaudeRuntimeState } from "./claudeSync";
|
|
9
|
+
import { RETIRED_PERMISSION_RULES } from "./claudeRetired";
|
|
10
|
+
import { p } from "./exec";
|
|
11
|
+
import type { Ctx } from "./index";
|
|
12
|
+
import { isObject, jqStringify, parseJson, type Json } from "./jq";
|
|
13
|
+
|
|
14
|
+
const REMOVED_MANIFEST = {
|
|
15
|
+
hooks: ["disable-claudeai-connectors.sh"],
|
|
16
|
+
files: ["alert_bubble.mp3"],
|
|
17
|
+
settingsKeys: [
|
|
18
|
+
"showTurnDuration",
|
|
19
|
+
"advisorModel",
|
|
20
|
+
"env.CLAUDE_CODE_SUBAGENT_MODEL",
|
|
21
|
+
"env.ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
22
|
+
"env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE",
|
|
23
|
+
"env.CLAUDE_CODE_DISABLE_1M_CONTEXT",
|
|
24
|
+
"env.CLAUDE_CODE_FORK_SUBAGENT",
|
|
25
|
+
"env.CLAUDE_CODE_EFFORT_LEVEL",
|
|
26
|
+
"enabledPlugins.effect-kit@docks",
|
|
27
|
+
"enabledPlugins.session-relay@docks",
|
|
28
|
+
"hooks.PreToolUse",
|
|
29
|
+
],
|
|
30
|
+
permissionRules: {
|
|
31
|
+
allow: ["Write(./)", "Bash(rtk *)", ...RETIRED_PERMISSION_RULES.allow],
|
|
32
|
+
deny: [
|
|
33
|
+
"Write(**/.env)",
|
|
34
|
+
"Write(**/.env.local)",
|
|
35
|
+
"Write(**/secrets/**)",
|
|
36
|
+
...RETIRED_PERMISSION_RULES.deny,
|
|
37
|
+
],
|
|
38
|
+
ask: [...RETIRED_PERMISSION_RULES.ask],
|
|
39
|
+
},
|
|
40
|
+
claudeJsonKeys: [] as Array<string>,
|
|
41
|
+
/** Home-relative artifacts the kit installed outside ~/.claude. */
|
|
42
|
+
homeFiles: [".local/bin/effect-solutions", ".local/bin/session-relay"],
|
|
43
|
+
runtimeReady: {
|
|
44
|
+
hooks: ["notify.sh"],
|
|
45
|
+
files: ["statusline.sh", "fetch-usage.sh"],
|
|
46
|
+
settingsKeys: ["hooks.Stop"],
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** claude::_prune_json_keys — present-count; deletes when !dryRun. */
|
|
51
|
+
function pruneJsonKeys(ctx: Ctx, file: string, keys: Array<string>): number {
|
|
52
|
+
if (keys.length === 0 || !existsSync(file)) return 0;
|
|
53
|
+
const doc = parseJson(readFileSync(file, "utf8"));
|
|
54
|
+
if (doc === undefined) return 0;
|
|
55
|
+
|
|
56
|
+
const hasPath = (root: Json, path: Array<string>): boolean => {
|
|
57
|
+
let cur: Json = root;
|
|
58
|
+
for (const seg of path.slice(0, -1)) {
|
|
59
|
+
if (!isObject(cur) || cur[seg] === undefined) return false;
|
|
60
|
+
cur = cur[seg]!;
|
|
61
|
+
}
|
|
62
|
+
return isObject(cur) && Object.prototype.hasOwnProperty.call(cur, path[path.length - 1]!);
|
|
63
|
+
};
|
|
64
|
+
const presentKeys = keys.filter((k) => hasPath(doc, k.split(".")));
|
|
65
|
+
if (presentKeys.length === 0) return 0;
|
|
66
|
+
|
|
67
|
+
if (!ctx.dryRun) {
|
|
68
|
+
for (const k of presentKeys) {
|
|
69
|
+
const path = k.split(".");
|
|
70
|
+
let cur: Json = doc;
|
|
71
|
+
for (const seg of path.slice(0, -1)) {
|
|
72
|
+
if (!isObject(cur)) break;
|
|
73
|
+
cur = cur[seg]!;
|
|
74
|
+
}
|
|
75
|
+
if (isObject(cur)) delete cur[path[path.length - 1]!];
|
|
76
|
+
}
|
|
77
|
+
writeFileSync(`${file}.tmp`, jqStringify(doc));
|
|
78
|
+
renameSync(`${file}.tmp`, file);
|
|
79
|
+
}
|
|
80
|
+
return presentKeys.length;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function prunePermissionRules(
|
|
84
|
+
ctx: Ctx,
|
|
85
|
+
file: string,
|
|
86
|
+
rules: Readonly<Record<"allow" | "deny" | "ask", ReadonlyArray<string>>>,
|
|
87
|
+
): number {
|
|
88
|
+
if (!existsSync(file)) return 0;
|
|
89
|
+
const doc = parseJson(readFileSync(file, "utf8"));
|
|
90
|
+
if (doc === undefined || !isObject(doc) || !isObject(doc["permissions"])) return 0;
|
|
91
|
+
const permissions = doc["permissions"];
|
|
92
|
+
let present = 0;
|
|
93
|
+
for (const key of ["allow", "deny", "ask"] as const) {
|
|
94
|
+
const values = permissions[key];
|
|
95
|
+
if (!Array.isArray(values)) continue;
|
|
96
|
+
const removed = new Set(rules[key]);
|
|
97
|
+
present += rules[key].filter((rule) => values.includes(rule)).length;
|
|
98
|
+
if (!ctx.dryRun)
|
|
99
|
+
permissions[key] = values.filter((value) => typeof value !== "string" || !removed.has(value));
|
|
100
|
+
}
|
|
101
|
+
if (present > 0 && !ctx.dryRun) {
|
|
102
|
+
writeFileSync(`${file}.tmp`, jqStringify(doc));
|
|
103
|
+
renameSync(`${file}.tmp`, file);
|
|
104
|
+
}
|
|
105
|
+
return present;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function syncRemovals(ctx: Ctx, claudeDir: string, runtime: ClaudeRuntimeState): void {
|
|
109
|
+
const { change, echo } = ctx.services.logger;
|
|
110
|
+
let hooksRemoved = 0;
|
|
111
|
+
let filesRemoved = 0;
|
|
112
|
+
const hooks = [
|
|
113
|
+
...REMOVED_MANIFEST.hooks,
|
|
114
|
+
...(runtime.kind === "ready" ? REMOVED_MANIFEST.runtimeReady.hooks : []),
|
|
115
|
+
];
|
|
116
|
+
const files = [
|
|
117
|
+
...REMOVED_MANIFEST.files,
|
|
118
|
+
...(runtime.kind === "ready" ? REMOVED_MANIFEST.runtimeReady.files : []),
|
|
119
|
+
];
|
|
120
|
+
const settingsKeys = [
|
|
121
|
+
...REMOVED_MANIFEST.settingsKeys.filter(
|
|
122
|
+
(key) => key !== "advisorModel" || ctx.claudeAdvisor === "",
|
|
123
|
+
),
|
|
124
|
+
...(runtime.kind === "ready" ? REMOVED_MANIFEST.runtimeReady.settingsKeys : []),
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
for (const name of hooks) {
|
|
128
|
+
const path = p(claudeDir, "hooks", name);
|
|
129
|
+
if (!existsSync(path)) continue;
|
|
130
|
+
if (ctx.dryRun) {
|
|
131
|
+
echo(`[dry-run] rm ${path}`);
|
|
132
|
+
} else {
|
|
133
|
+
rmSync(path, { force: true });
|
|
134
|
+
hooksRemoved++;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (!ctx.dryRun && hooksRemoved > 0) {
|
|
138
|
+
try {
|
|
139
|
+
rmdirSync(p(claudeDir, "hooks"));
|
|
140
|
+
} catch {
|
|
141
|
+
// Preserve a non-empty user hooks directory.
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const rel of files) {
|
|
146
|
+
const path = p(claudeDir, rel);
|
|
147
|
+
if (!existsSync(path)) continue;
|
|
148
|
+
if (ctx.dryRun) {
|
|
149
|
+
echo(`[dry-run] rm ${path}`);
|
|
150
|
+
} else {
|
|
151
|
+
rmSync(path, { force: true });
|
|
152
|
+
filesRemoved++;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const rel of REMOVED_MANIFEST.homeFiles) {
|
|
157
|
+
const path = p(ctx.home, rel);
|
|
158
|
+
if (!existsSync(path)) continue;
|
|
159
|
+
if (ctx.dryRun) {
|
|
160
|
+
echo(`[dry-run] rm ${path}`);
|
|
161
|
+
} else {
|
|
162
|
+
rmSync(path, { force: true });
|
|
163
|
+
filesRemoved++;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const skeys = pruneJsonKeys(ctx, p(claudeDir, "settings.json"), settingsKeys);
|
|
168
|
+
const permissionRules = prunePermissionRules(
|
|
169
|
+
ctx,
|
|
170
|
+
p(claudeDir, "settings.json"),
|
|
171
|
+
REMOVED_MANIFEST.permissionRules,
|
|
172
|
+
);
|
|
173
|
+
const cjkeys = pruneJsonKeys(ctx, p(ctx.home, ".claude.json"), REMOVED_MANIFEST.claudeJsonKeys);
|
|
174
|
+
|
|
175
|
+
if (ctx.dryRun) {
|
|
176
|
+
if (skeys > 0)
|
|
177
|
+
echo(`[dry-run] del ${skeys} stale key(s) from ${p(claudeDir, "settings.json")}`);
|
|
178
|
+
if (permissionRules > 0) {
|
|
179
|
+
echo(
|
|
180
|
+
`[dry-run] del ${permissionRules} stale permission rule(s) from ${p(claudeDir, "settings.json")}`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
if (cjkeys > 0)
|
|
184
|
+
echo(`[dry-run] del ${cjkeys} stale key(s) from ${p(ctx.home, ".claude.json")}`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (hooksRemoved + filesRemoved + skeys + permissionRules + cjkeys > 0) {
|
|
189
|
+
const permissionSummary = permissionRules > 0 ? `, permission rules: ${permissionRules}` : "";
|
|
190
|
+
change(
|
|
191
|
+
`Pruned stale artifacts (hooks: ${hooksRemoved}, files: ${filesRemoved}, settings keys: ${skeys}, claude.json keys: ${cjkeys}${permissionSummary})`,
|
|
192
|
+
);
|
|
193
|
+
ctx.nextStepTriggers.claudeRestart = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -143,7 +143,7 @@ export const RETIRED_PERMISSION_RULES = {
|
|
|
143
143
|
"PowerShell(python *)",
|
|
144
144
|
"PowerShell(pip *)",
|
|
145
145
|
"PowerShell(pip3 *)",
|
|
146
|
-
"WebFetch"
|
|
146
|
+
"WebFetch",
|
|
147
147
|
],
|
|
148
148
|
deny: [
|
|
149
149
|
"PowerShell(Remove-Item *-Recurse* \\)",
|
|
@@ -159,7 +159,7 @@ export const RETIRED_PERMISSION_RULES = {
|
|
|
159
159
|
"PowerShell(rm *-Recurse* \\)",
|
|
160
160
|
"PowerShell(rm *-Recurse* *:\\)",
|
|
161
161
|
"PowerShell(rmdir *-Recurse* \\)",
|
|
162
|
-
"PowerShell(rmdir *-Recurse* *:\\)"
|
|
162
|
+
"PowerShell(rmdir *-Recurse* *:\\)",
|
|
163
163
|
],
|
|
164
|
-
ask: []
|
|
165
|
-
} as const satisfies Readonly<Record<"allow" | "deny" | "ask", ReadonlyArray<string
|
|
164
|
+
ask: [],
|
|
165
|
+
} as const satisfies Readonly<Record<"allow" | "deny" | "ask", ReadonlyArray<string>>>;
|