opensocrates 1.3.1 → 1.5.0

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.
@@ -15,6 +15,7 @@ import {
15
15
  chmod,
16
16
  chown,
17
17
  cp,
18
+ copyFile,
18
19
  lstat,
19
20
  mkdir,
20
21
  mkdtemp,
@@ -34,27 +35,22 @@ import { Readable } from "node:stream";
34
35
  import { pipeline } from "node:stream/promises";
35
36
  import { fileURLToPath } from "node:url";
36
37
 
37
- export const PRODUCT_VERSION = "1.3.1";
38
+ export const PRODUCT_VERSION = "1.5.0";
38
39
  export const REPOSITORY = "ParkerHwang/OpenSocrates";
39
40
  export const MARKETPLACE_NAME = "opensocrates";
40
41
  export const PLUGIN_NAME = "opensocrates";
41
42
  export const PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
42
43
  export const DEFAULT_HOST = "codex";
43
- export const SUPPORTED_HOSTS = Object.freeze([
44
- "antigravity",
45
- "claude",
46
- "codex",
47
- "cursor",
48
- "grok",
49
- "opencode",
50
- ]);
44
+ export const SUPPORTED_HOSTS = Object.freeze(["codex"]);
45
+ export const MACOS_PROFILE_HOSTS = Object.freeze(["claude", "antigravity", "claude-chat"]);
51
46
  export const ALL_HOST = "all";
52
- export const HOST_CHOICES = Object.freeze([ALL_HOST, ...SUPPORTED_HOSTS]);
47
+ export const HOST_CHOICES = Object.freeze([ALL_HOST, ...SUPPORTED_HOSTS, ...MACOS_PROFILE_HOSTS]);
53
48
  export function assetNameFor(host = DEFAULT_HOST) {
54
49
  if (!SUPPORTED_HOSTS.includes(host)) {
55
50
  fail(`unsupported host ${JSON.stringify(host)}`);
56
51
  }
57
- return `opensocrates-${PRODUCT_VERSION}-${host}-plugin.zip`;
52
+ const suffix = process.platform === "win32" ? "-windows-x64" : "";
53
+ return `opensocrates-${PRODUCT_VERSION}-${host}-plugin${suffix}.zip`;
58
54
  }
59
55
  export const ASSET_NAME = assetNameFor(DEFAULT_HOST);
60
56
  export const CHECKSUM_NAME = `${ASSET_NAME}.sha256`;
@@ -65,87 +61,18 @@ const CODEX_MARKER = Object.freeze({
65
61
  marketplaceName: MARKETPLACE_NAME,
66
62
  pluginName: PLUGIN_NAME,
67
63
  });
68
- const CLAUDE_MARKER = Object.freeze({
69
- schemaVersion: 1,
70
- marketplaceName: MARKETPLACE_NAME,
71
- pluginName: PLUGIN_NAME,
72
- host: "claude",
73
- });
74
- const CURSOR_MARKER = Object.freeze({
75
- schemaVersion: 1,
76
- marketplaceName: MARKETPLACE_NAME,
77
- pluginName: PLUGIN_NAME,
78
- host: "cursor",
79
- registrationKind: "file-drop",
80
- });
81
- const ANTIGRAVITY_MARKER = Object.freeze({
82
- schemaVersion: 1,
83
- marketplaceName: MARKETPLACE_NAME,
84
- pluginName: PLUGIN_NAME,
85
- host: "antigravity",
86
- registrationKind: "file-drop",
87
- });
88
- const GROK_MARKER = Object.freeze({
89
- schemaVersion: 1,
90
- marketplaceName: MARKETPLACE_NAME,
91
- pluginName: PLUGIN_NAME,
92
- host: "grok",
93
- registrationKind: "file-drop",
94
- });
95
- const OPENCODE_MARKER = Object.freeze({
96
- schemaVersion: 1,
97
- marketplaceName: MARKETPLACE_NAME,
98
- pluginName: PLUGIN_NAME,
99
- host: "opencode",
100
- registrationKind: "automatic-file-discovery",
101
- });
102
- const OPENCODE_INSTALL_MANIFEST = ".opensocrates-installation.json";
103
- const OPENCODE_BRIDGE_MARKER = ".opensocrates-managed.json";
104
- // OpenCode is discovered automatically from its config directory, so it is
105
- // deliberately not a file-drop host.
106
- const FILE_DROP_HOSTS = Object.freeze(["antigravity", "cursor", "grok"]);
107
64
  const HOST_LAYOUTS = Object.freeze({
108
- antigravity: {
109
- marketplaceRelative: null,
110
- pluginRelative: ".",
111
- manifestRelative: "plugin.json",
112
- requiresRuntime: false,
113
- },
114
- claude: {
115
- marketplaceRelative: join(".claude-plugin", "marketplace.json"),
116
- pluginRelative: join("plugins", PLUGIN_NAME),
117
- manifestRelative: join(".claude-plugin", "plugin.json"),
118
- requiresRuntime: true,
119
- },
120
65
  codex: {
121
66
  marketplaceRelative: join(".agents", "plugins", "marketplace.json"),
122
67
  pluginRelative: join("build", "generated", "plugins", "codex"),
123
68
  manifestRelative: join(".codex-plugin", "plugin.json"),
124
69
  requiresRuntime: true,
125
70
  },
126
- cursor: {
127
- marketplaceRelative: null,
128
- pluginRelative: ".",
129
- manifestRelative: "plugin.json",
130
- requiresRuntime: false,
131
- },
132
- grok: {
133
- marketplaceRelative: null,
134
- pluginRelative: ".",
135
- manifestRelative: "plugin.json",
136
- requiresRuntime: false,
137
- },
138
- opencode: {
139
- marketplaceRelative: null,
140
- pluginRelative: ".",
141
- manifestRelative: "opencode-plugin.json",
142
- requiresRuntime: false,
143
- },
144
71
  });
145
72
  const MAX_DOWNLOAD_BYTES = 2 * 1024 * 1024 * 1024;
146
73
  const MAX_ARCHIVE_ENTRIES = 10_000;
147
74
  const DESIRED_STATE_SCHEMA = "opensocrates.desired-state/1.0.0";
148
- const RECEIPT_SCHEMA = "opensocrates.auto-update-receipt/1.0.0";
75
+ const RECEIPT_SCHEMA = "opensocrates.auto-update-receipt/1.1.0";
149
76
  const AUTO_UPDATE_LABEL = "com.opensocrates.auto-update";
150
77
  const AUTO_UPDATE_MIN_INTERVAL_HOURS = 1;
151
78
  const AUTO_UPDATE_MAX_INTERVAL_HOURS = 24 * 7;
@@ -174,17 +101,12 @@ const MAX_CODEX_APP_SERVER_NOTIFICATIONS = 128;
174
101
  // Keep host security state separate from installer-owned payload cleanup so an
175
102
  // explicit trust reset can fail without weakening purge path-ownership checks.
176
103
  export function purgeExtensionResult(host, resetTrust = false) {
177
- return host === "codex"
178
- ? {
179
- component: "host-security-trust",
180
- status: resetTrust ? "pending" : "preserved",
181
- nextAction: resetTrust ? null : "rerun-purge-with-reset-trust",
182
- }
183
- : {
184
- component: "host-security-trust",
185
- status: "not-applicable",
186
- nextAction: null,
187
- };
104
+ if (host !== "codex") fail("only Codex is supported");
105
+ return {
106
+ component: "host-security-trust",
107
+ status: resetTrust ? "pending" : "preserved",
108
+ nextAction: resetTrust ? null : "rerun-purge-with-reset-trust",
109
+ };
188
110
  }
189
111
 
190
112
  export function createPurgeResult(hosts, { resetTrust = false } = {}) {
@@ -259,7 +181,11 @@ function decodeTomlLiteralString(source, start) {
259
181
  const end = source.indexOf("'", start + 1);
260
182
  if (end < 0) return null;
261
183
  const value = source.slice(start + 1, end);
262
- if ([...value].some((character) => character.codePointAt(0) < 0x20 || character.codePointAt(0) === 0x7f)) {
184
+ if (
185
+ [...value].some(
186
+ (character) => character.codePointAt(0) < 0x20 || character.codePointAt(0) === 0x7f,
187
+ )
188
+ ) {
263
189
  return null;
264
190
  }
265
191
  return { value, next: end + 1, kind: "literal", raw: source.slice(start, end + 1) };
@@ -374,7 +300,12 @@ function tomlLines(source) {
374
300
  while (start < source.length) {
375
301
  const newline = source.indexOf("\n", start);
376
302
  const end = newline < 0 ? source.length : newline + 1;
377
- const contentEnd = newline < 0 ? source.length : newline > start && source[newline - 1] === "\r" ? newline - 1 : newline;
303
+ const contentEnd =
304
+ newline < 0
305
+ ? source.length
306
+ : newline > start && source[newline - 1] === "\r"
307
+ ? newline - 1
308
+ : newline;
378
309
  lines.push({ start, end, contentEnd, body: source.slice(start, contentEnd) });
379
310
  start = end;
380
311
  }
@@ -386,7 +317,10 @@ function parseTomlTableHeader(code) {
386
317
  const trimmedEnd = code.trimEnd().length;
387
318
  const candidate = code.slice(leading, trimmedEnd);
388
319
  const array = candidate.startsWith("[[");
389
- if (!candidate.startsWith("[") || (array ? !candidate.endsWith("]]") : !candidate.endsWith("]"))) {
320
+ if (
321
+ !candidate.startsWith("[") ||
322
+ (array ? !candidate.endsWith("]]") : !candidate.endsWith("]"))
323
+ ) {
390
324
  return null;
391
325
  }
392
326
  const openWidth = array ? 2 : 1;
@@ -532,7 +466,8 @@ export function stripCodexOpenSocratesTrustSections(contents) {
532
466
  if (
533
467
  pathContainsTrustNamespace(semanticPath) ||
534
468
  ((semanticPath[0]?.value === "hooks" ||
535
- (currentTableSegments[0]?.value === "hooks" && currentTableSegments[1]?.value === "state")) &&
469
+ (currentTableSegments[0]?.value === "hooks" &&
470
+ currentTableSegments[1]?.value === "state")) &&
536
471
  lineContainsTrustNamespaceString(code))
537
472
  ) {
538
473
  trustResetFailure("the matching trust namespace uses a noncanonical assignment shape");
@@ -589,7 +524,12 @@ function currentUid() {
589
524
  }
590
525
 
591
526
  function assertOwnedDirectory(info) {
592
- if (!info.isDirectory() || info.isSymbolicLink() || info.uid !== currentUid() || (info.mode & 0o022) !== 0) {
527
+ if (
528
+ !info.isDirectory() ||
529
+ info.isSymbolicLink() ||
530
+ info.uid !== currentUid() ||
531
+ (info.mode & 0o022) !== 0
532
+ ) {
593
533
  trustResetFailure("the Codex configuration directory is not a safe owner-controlled directory");
594
534
  }
595
535
  }
@@ -756,16 +696,20 @@ function exactCodexAppServerNotification(message) {
756
696
  const keys = Object.keys(message).sort();
757
697
  const withoutTimestamp = keys.length === 2 && keys[0] === "method" && keys[1] === "params";
758
698
  const withTimestamp =
759
- keys.length === 3 &&
760
- keys[0] === "emittedAtMs" &&
761
- keys[1] === "method" &&
762
- keys[2] === "params";
699
+ keys.length === 3 && keys[0] === "emittedAtMs" && keys[1] === "method" && keys[2] === "params";
763
700
  if (!withoutTimestamp && !withTimestamp) return false;
764
701
  if (typeof message.method !== "string" || message.method.length === 0) return false;
765
- if (message.params === null || typeof message.params !== "object" || Array.isArray(message.params)) {
702
+ if (
703
+ message.params === null ||
704
+ typeof message.params !== "object" ||
705
+ Array.isArray(message.params)
706
+ ) {
766
707
  return false;
767
708
  }
768
- return !withTimestamp || (typeof message.emittedAtMs === "number" && Number.isFinite(message.emittedAtMs));
709
+ return (
710
+ !withTimestamp ||
711
+ (typeof message.emittedAtMs === "number" && Number.isFinite(message.emittedAtMs))
712
+ );
769
713
  }
770
714
 
771
715
  function codexAppServerValidationError() {
@@ -1030,7 +974,9 @@ async function validateCodexConfigBytes(
1030
974
  await removeValidationHome(validationHome, { recursive: true, force: true });
1031
975
  } catch {
1032
976
  if (!operationFailed) {
1033
- trustResetFailure("the isolated Codex validation environment could not be removed safely");
977
+ trustResetFailure(
978
+ "the isolated Codex validation environment could not be removed safely",
979
+ );
1034
980
  }
1035
981
  }
1036
982
  }
@@ -1184,7 +1130,9 @@ export async function resetCodexOpenSocratesHookTrust({
1184
1130
  });
1185
1131
  }
1186
1132
  if (error instanceof InstallerError) throw error;
1187
- trustResetFailure("the atomic configuration update failed; the original configuration was preserved");
1133
+ trustResetFailure(
1134
+ "the atomic configuration update failed; the original configuration was preserved",
1135
+ );
1188
1136
  } finally {
1189
1137
  let cleanupFailed = false;
1190
1138
  if (cleanupRequired) {
@@ -1206,6 +1154,23 @@ export async function resetCodexOpenSocratesHookTrust({
1206
1154
 
1207
1155
  export class InstallerError extends Error {}
1208
1156
 
1157
+ class PackagePreparationError extends InstallerError {
1158
+ constructor(outcomes) {
1159
+ const failures = outcomes.filter((item) => item.error !== null);
1160
+ super(
1161
+ "package preparation failed: " +
1162
+ failures
1163
+ .map((item) => `${item.host}: ${String(item.error?.message ?? item.error)}`)
1164
+ .join("; "),
1165
+ );
1166
+ this.hostOutcomes = outcomes.map(({ host, error }) => ({
1167
+ host,
1168
+ result: "failed",
1169
+ errorCategory: error === null ? null : errorCategory(error),
1170
+ }));
1171
+ }
1172
+ }
1173
+
1209
1174
  function fail(message) {
1210
1175
  throw new InstallerError(message);
1211
1176
  }
@@ -1245,7 +1210,9 @@ async function requireRegularFileEntry(target, label) {
1245
1210
  }
1246
1211
 
1247
1212
  export function statePaths() {
1248
- const stateDirectory = resolve(process.env.OPENSOCRATES_STATE_DIR || join(homedir(), ".opensocrates"));
1213
+ const stateDirectory = resolve(
1214
+ process.env.OPENSOCRATES_STATE_DIR || join(homedir(), ".opensocrates"),
1215
+ );
1249
1216
  const launchAgentsDirectory = resolve(
1250
1217
  process.env.OPENSOCRATES_LAUNCH_AGENTS_DIR || join(homedir(), "Library", "LaunchAgents"),
1251
1218
  );
@@ -1281,7 +1248,12 @@ function defaultDesiredState() {
1281
1248
  }
1282
1249
 
1283
1250
  function normalizeDesiredState(value) {
1284
- if (value === null || typeof value !== "object" || Array.isArray(value) || value.schema !== DESIRED_STATE_SCHEMA) {
1251
+ if (
1252
+ value === null ||
1253
+ typeof value !== "object" ||
1254
+ Array.isArray(value) ||
1255
+ value.schema !== DESIRED_STATE_SCHEMA
1256
+ ) {
1285
1257
  fail("the OpenSocrates desired-state manifest has an unsupported schema");
1286
1258
  }
1287
1259
  const installedHosts = value.installedHosts;
@@ -1290,7 +1262,9 @@ function normalizeDesiredState(value) {
1290
1262
  installedHosts.some((host) => !SUPPORTED_HOSTS.includes(host)) ||
1291
1263
  new Set(installedHosts).size !== installedHosts.length
1292
1264
  ) {
1293
- fail("the OpenSocrates desired-state manifest has an invalid host set");
1265
+ fail(
1266
+ "the OpenSocrates desired-state manifest has an invalid or retired host set; uninstall non-Codex hosts using opensocrates@1.3.1 before upgrading",
1267
+ );
1294
1268
  }
1295
1269
  const channel = value.channel;
1296
1270
  if (!new Set(["stable", "next"]).has(channel)) {
@@ -1307,10 +1281,16 @@ function normalizeDesiredState(value) {
1307
1281
  fail("the OpenSocrates desired-state manifest has an invalid update policy");
1308
1282
  }
1309
1283
  const autoUpdateHosts =
1310
- value.autoUpdate.hosts === undefined ? (value.autoUpdate.enabled ? installedHosts : []) : value.autoUpdate.hosts;
1284
+ value.autoUpdate.hosts === undefined
1285
+ ? value.autoUpdate.enabled
1286
+ ? installedHosts
1287
+ : []
1288
+ : value.autoUpdate.hosts;
1311
1289
  if (
1312
1290
  !Array.isArray(autoUpdateHosts) ||
1313
- autoUpdateHosts.some((host) => !SUPPORTED_HOSTS.includes(host) || !installedHosts.includes(host)) ||
1291
+ autoUpdateHosts.some(
1292
+ (host) => !SUPPORTED_HOSTS.includes(host) || !installedHosts.includes(host),
1293
+ ) ||
1314
1294
  new Set(autoUpdateHosts).size !== autoUpdateHosts.length ||
1315
1295
  (value.autoUpdate.enabled && autoUpdateHosts.length === 0) ||
1316
1296
  (!value.autoUpdate.enabled && autoUpdateHosts.length > 0)
@@ -1354,7 +1334,8 @@ async function ensurePrivateDirectory(directory) {
1354
1334
  if (!info.isDirectory() || info.isSymbolicLink()) {
1355
1335
  fail(`refusing to use a non-directory or symbolic-link state path: ${directory}`);
1356
1336
  }
1357
- await chmod(directory, 0o700);
1337
+ if (process.platform === "win32") windowsAction("private", directory);
1338
+ else await chmod(directory, 0o700);
1358
1339
  return;
1359
1340
  }
1360
1341
  await mkdir(directory, { recursive: true, mode: 0o700 });
@@ -1362,7 +1343,8 @@ async function ensurePrivateDirectory(directory) {
1362
1343
  if (!info.isDirectory() || info.isSymbolicLink()) {
1363
1344
  fail(`refusing to use an unsafe state path: ${directory}`);
1364
1345
  }
1365
- await chmod(directory, 0o700);
1346
+ if (process.platform === "win32") windowsAction("private", directory);
1347
+ else await chmod(directory, 0o700);
1366
1348
  }
1367
1349
 
1368
1350
  async function atomicWritePrivateFile(target, contents) {
@@ -1375,7 +1357,8 @@ async function atomicWritePrivateFile(target, contents) {
1375
1357
  mode: 0o600,
1376
1358
  flag: "wx",
1377
1359
  });
1378
- await chmod(temporary, 0o600);
1360
+ if (process.platform === "win32") windowsAction("private", temporary);
1361
+ else await chmod(temporary, 0o600);
1379
1362
  await rename(temporary, target);
1380
1363
  } finally {
1381
1364
  if (await exists(temporary)) {
@@ -1386,7 +1369,10 @@ async function atomicWritePrivateFile(target, contents) {
1386
1369
 
1387
1370
  export async function writeDesiredState(value) {
1388
1371
  const normalized = normalizeDesiredState(value);
1389
- await atomicWritePrivateFile(statePaths().desiredState, `${JSON.stringify(normalized, null, 2)}\n`);
1372
+ await atomicWritePrivateFile(
1373
+ statePaths().desiredState,
1374
+ `${JSON.stringify(normalized, null, 2)}\n`,
1375
+ );
1390
1376
  return normalized;
1391
1377
  }
1392
1378
 
@@ -1397,9 +1383,10 @@ async function writeAutoUpdateReceipt({ version, checkedAt, hosts, result, error
1397
1383
  checkedAt,
1398
1384
  hosts: [...hosts]
1399
1385
  .sort((left, right) => left.host.localeCompare(right.host))
1400
- .map(({ host, result: hostResult }) => ({
1386
+ .map(({ host, result: hostResult, errorCategory: hostErrorCategory }) => ({
1401
1387
  host,
1402
1388
  result: hostResult,
1389
+ errorCategory: hostErrorCategory ?? null,
1403
1390
  })),
1404
1391
  result,
1405
1392
  errorCategory: errorCategory ?? null,
@@ -1414,7 +1401,9 @@ function nowIso() {
1414
1401
  function nextCheckAt(intervalHours) {
1415
1402
  const intervalMilliseconds = intervalHours * 60 * 60 * 1000;
1416
1403
  const jitterBasisPoints = randomInt(-1500, 1501);
1417
- return new Date(Date.now() + intervalMilliseconds * (1 + jitterBasisPoints / 10_000)).toISOString();
1404
+ return new Date(
1405
+ Date.now() + intervalMilliseconds * (1 + jitterBasisPoints / 10_000),
1406
+ ).toISOString();
1418
1407
  }
1419
1408
 
1420
1409
  function majorVersion(value) {
@@ -1422,11 +1411,24 @@ function majorVersion(value) {
1422
1411
  return match ? Number(match[1]) : null;
1423
1412
  }
1424
1413
 
1425
- function errorCategory(error) {
1426
- const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
1427
- if (/checksum|manifest|archive|symbolic link/u.test(message)) return "verification";
1414
+ export function errorCategory(error) {
1415
+ if (error instanceof PackagePreparationError) {
1416
+ const categories = new Set(
1417
+ error.hostOutcomes.map((item) => item.errorCategory).filter((item) => item !== null),
1418
+ );
1419
+ return categories.size === 1 ? [...categories][0] : "multiple";
1420
+ }
1421
+ const message =
1422
+ error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
1423
+ if (
1424
+ /checksum|manifest|archive|symbolic link|target metadata|runtime layout|unexpected native runtime or launcher surface/u.test(
1425
+ message,
1426
+ )
1427
+ )
1428
+ return "verification";
1428
1429
  if (/download|fetch|network|offline|timed? ?out/u.test(message)) return "network";
1429
- if (/preflight|not logged|auth|version is required|could not run/u.test(message)) return "preflight";
1430
+ if (/preflight|not logged|auth|version is required|could not run/u.test(message))
1431
+ return "preflight";
1430
1432
  if (/rollback|restore/u.test(message)) return "rollback";
1431
1433
  if (/lock|already running/u.test(message)) return "locked";
1432
1434
  if (/register|plugin|marketplace/u.test(message)) return "activation";
@@ -1440,7 +1442,10 @@ async function acquireOperationLock() {
1440
1442
  try {
1441
1443
  const handle = await open(paths.lock, "wx", 0o600);
1442
1444
  try {
1443
- await handle.writeFile(`${JSON.stringify({ pid: process.pid, startedAt: nowIso() })}\n`, "utf8");
1445
+ await handle.writeFile(
1446
+ `${JSON.stringify({ pid: process.pid, startedAt: nowIso() })}\n`,
1447
+ "utf8",
1448
+ );
1444
1449
  } catch (writeError) {
1445
1450
  await handle.close().catch(() => undefined);
1446
1451
  await rm(paths.lock, { force: true }).catch(() => undefined);
@@ -1472,7 +1477,9 @@ async function acquireOperationLock() {
1472
1477
  }
1473
1478
  stale =
1474
1479
  ownerAlive === false ||
1475
- (ownerAlive === null && Number.isFinite(started) && Date.now() - started > LOCK_STALE_MILLISECONDS);
1480
+ (ownerAlive === null &&
1481
+ Number.isFinite(started) &&
1482
+ Date.now() - started > LOCK_STALE_MILLISECONDS);
1476
1483
  } catch {
1477
1484
  const info = await lstat(paths.lock);
1478
1485
  stale = Date.now() - info.mtimeMs > LOCK_STALE_MILLISECONDS;
@@ -1514,34 +1521,29 @@ function jsonDeepEqual(left, right) {
1514
1521
  left.every((item, index) => jsonDeepEqual(item, right[index]))
1515
1522
  );
1516
1523
  }
1517
- if (
1518
- left === null ||
1519
- right === null ||
1520
- typeof left !== "object" ||
1521
- typeof right !== "object"
1522
- ) {
1524
+ if (left === null || right === null || typeof left !== "object" || typeof right !== "object") {
1523
1525
  return false;
1524
1526
  }
1525
1527
  const leftKeys = Object.keys(left).sort();
1526
1528
  const rightKeys = Object.keys(right).sort();
1527
1529
  return (
1528
1530
  leftKeys.length === rightKeys.length &&
1529
- leftKeys.every(
1530
- (key, index) => key === rightKeys[index] && jsonDeepEqual(left[key], right[key]),
1531
- )
1531
+ leftKeys.every((key, index) => key === rightKeys[index] && jsonDeepEqual(left[key], right[key]))
1532
1532
  );
1533
1533
  }
1534
1534
 
1535
1535
  function markerFor(host) {
1536
- if (host === "cursor") return CURSOR_MARKER;
1537
- if (host === "antigravity") return ANTIGRAVITY_MARKER;
1538
- if (host === "grok") return GROK_MARKER;
1539
- if (host === "opencode") return OPENCODE_MARKER;
1540
- return host === "claude" ? CLAUDE_MARKER : CODEX_MARKER;
1536
+ return CODEX_MARKER;
1541
1537
  }
1542
1538
 
1543
1539
  export function markerMatches(value, host = DEFAULT_HOST) {
1544
- return value !== null && typeof value === "object" && !Array.isArray(value) && jsonEqual(value, markerFor(host));
1540
+ if (host !== "codex") return false;
1541
+ return (
1542
+ value !== null &&
1543
+ typeof value === "object" &&
1544
+ !Array.isArray(value) &&
1545
+ jsonEqual(value, markerFor(host))
1546
+ );
1545
1547
  }
1546
1548
 
1547
1549
  export function isSafeArchivePath(value) {
@@ -1558,7 +1560,17 @@ export function isSafeArchivePath(value) {
1558
1560
  if (candidate.length === 0) {
1559
1561
  return false;
1560
1562
  }
1561
- return candidate.split("/").every((part) => part.length > 0 && part !== "." && part !== "..");
1563
+ return candidate
1564
+ .split("/")
1565
+ .every(
1566
+ (part) =>
1567
+ part.length > 0 &&
1568
+ part !== "." &&
1569
+ part !== ".." &&
1570
+ !/[\x00-\x1f<>:"|?*]/u.test(part) &&
1571
+ !/[. ]$/u.test(part) &&
1572
+ !/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/iu.test(part),
1573
+ );
1562
1574
  }
1563
1575
 
1564
1576
  export function parseChecksumText(text, expectedName = ASSET_NAME) {
@@ -1581,7 +1593,19 @@ export function parseChecksumText(text, expectedName = ASSET_NAME) {
1581
1593
  }
1582
1594
 
1583
1595
  export function parseCli(argv) {
1584
- const actions = new Set(["install", "status", "update", "remove", "verify", "auto-update", "help"]);
1596
+ const actions = new Set([
1597
+ "install",
1598
+ "status",
1599
+ "update",
1600
+ "remove",
1601
+ "verify",
1602
+ "auto-update",
1603
+ "help",
1604
+ "diagnose",
1605
+ "disable",
1606
+ "enable",
1607
+ "export",
1608
+ ]);
1585
1609
  const args = [...argv];
1586
1610
  let action = "install";
1587
1611
  if (args[0] && !args[0].startsWith("-")) {
@@ -1608,13 +1632,17 @@ export function parseCli(argv) {
1608
1632
  host: action === "auto-update" ? ALL_HOST : DEFAULT_HOST,
1609
1633
  asset: null,
1610
1634
  checksum: null,
1611
- hostAssets: Object.fromEntries(SUPPORTED_HOSTS.map((host) => [host, { asset: null, checksum: null }])),
1635
+ hostAssets: Object.fromEntries(
1636
+ SUPPORTED_HOSTS.map((host) => [host, { asset: null, checksum: null }]),
1637
+ ),
1612
1638
  channel: "stable",
1613
1639
  intervalHours: AUTO_UPDATE_DEFAULT_INTERVAL_HOURS,
1614
1640
  allowMajor: false,
1615
1641
  force: false,
1616
1642
  purge: false,
1617
1643
  resetTrust: false,
1644
+ workspace: null,
1645
+ output: null,
1618
1646
  };
1619
1647
  const seenOptions = new Set();
1620
1648
  while (args.length > 0) {
@@ -1632,6 +1660,12 @@ export function parseCli(argv) {
1632
1660
  options.host = host;
1633
1661
  continue;
1634
1662
  }
1663
+ if (flag === "--workspace" || flag === "--output") {
1664
+ const value = args.shift();
1665
+ if (!value || value.startsWith("-")) fail(`${flag} requires a path`);
1666
+ options[flag === "--workspace" ? "workspace" : "output"] = resolve(value);
1667
+ continue;
1668
+ }
1635
1669
  if (flag === "--allow-major") {
1636
1670
  seenOptions.add("allow-major");
1637
1671
  options.allowMajor = true;
@@ -1678,7 +1712,9 @@ export function parseCli(argv) {
1678
1712
  options.intervalHours = value;
1679
1713
  continue;
1680
1714
  }
1681
- const qualified = flag?.match(new RegExp(`^--(asset|checksum)-(${SUPPORTED_HOSTS.join("|")})$`, "u"));
1715
+ const qualified = flag?.match(
1716
+ new RegExp(`^--(asset|checksum)-(${SUPPORTED_HOSTS.join("|")})$`, "u"),
1717
+ );
1682
1718
  if (qualified) {
1683
1719
  seenOptions.add(qualified[1]);
1684
1720
  const value = args.shift();
@@ -1710,10 +1746,13 @@ export function parseCli(argv) {
1710
1746
  if (options.host === ALL_HOST && options.asset !== null) {
1711
1747
  fail("--host all requires host-qualified --asset-<host> and --checksum-<host> options");
1712
1748
  }
1713
- if (options.host !== ALL_HOST && SUPPORTED_HOSTS.some((host) => options.hostAssets[host].asset !== null)) {
1749
+ if (
1750
+ options.host !== ALL_HOST &&
1751
+ SUPPORTED_HOSTS.some((host) => options.hostAssets[host].asset !== null)
1752
+ ) {
1714
1753
  fail("host-qualified asset options are only valid with --host all");
1715
1754
  }
1716
- if (["status", "remove", "help"].includes(options.action) && options.asset !== null) {
1755
+ if (["status", "remove", "help", "diagnose", "disable", "enable"].includes(options.action) && options.asset !== null) {
1717
1756
  fail(`${options.action} does not accept --asset or --checksum`);
1718
1757
  }
1719
1758
  if (
@@ -1725,11 +1764,13 @@ export function parseCli(argv) {
1725
1764
  if (
1726
1765
  options.action === "auto-update" &&
1727
1766
  options.autoUpdateAction !== "run" &&
1728
- (options.asset !== null || SUPPORTED_HOSTS.some((host) => options.hostAssets[host].asset !== null))
1767
+ (options.asset !== null ||
1768
+ SUPPORTED_HOSTS.some((host) => options.hostAssets[host].asset !== null))
1729
1769
  ) {
1730
1770
  fail(`auto-update ${options.autoUpdateAction} does not accept local asset options`);
1731
1771
  }
1732
- const autoUpdateEnable = options.action === "auto-update" && options.autoUpdateAction === "enable";
1772
+ const autoUpdateEnable =
1773
+ options.action === "auto-update" && options.autoUpdateAction === "enable";
1733
1774
  const autoUpdateRun = options.action === "auto-update" && options.autoUpdateAction === "run";
1734
1775
  for (const policyOption of ["allow-major", "channel", "interval-hours"]) {
1735
1776
  if (seenOptions.has(policyOption) && !autoUpdateEnable) {
@@ -1758,19 +1799,86 @@ export function parseCli(argv) {
1758
1799
  if (autoUpdateRun && seenOptions.has("host") && options.host !== ALL_HOST) {
1759
1800
  fail("auto-update run always reconciles desired state and only accepts --host all");
1760
1801
  }
1802
+ if (options.workspace !== null && options.host !== "antigravity") {
1803
+ fail("--workspace is only valid for Antigravity");
1804
+ }
1805
+ if (options.output !== null && !(options.host === "claude-chat" && options.action === "export")) {
1806
+ fail("--output is only valid for the Claude account export");
1807
+ }
1808
+ if (options.host === "claude-chat" && !["export", "verify", "help"].includes(options.action)) {
1809
+ fail("Claude account delivery uses export/verify; account activation is a separate UI action");
1810
+ }
1811
+ if (MACOS_PROFILE_HOSTS.includes(options.host) && (options.purge || options.resetTrust || options.action === "auto-update")) {
1812
+ fail("additional profiles do not broaden purge, trust reset or automatic updates");
1813
+ }
1814
+ if (!MACOS_PROFILE_HOSTS.includes(options.host) && ["diagnose", "disable", "enable", "export"].includes(options.action)) {
1815
+ fail(`${options.action} requires an additional profile`);
1816
+ }
1761
1817
  return options;
1762
1818
  }
1763
1819
 
1820
+ function profileAssetName(host) {
1821
+ if (!MACOS_PROFILE_HOSTS.includes(host)) fail("unknown content/native profile");
1822
+ const suffix = host === "claude-chat" ? "skills" : "plugin";
1823
+ return `opensocrates-${PRODUCT_VERSION}-${host}-${suffix}.zip`;
1824
+ }
1825
+
1826
+ async function prepareProfilePackage(host, options) {
1827
+ const scratch = await mkdtemp(join(tmpdir(), "opensocrates-profile-"));
1828
+ try {
1829
+ if (process.platform === "win32") windowsAction("seal-new", scratch);
1830
+ const name = profileAssetName(host);
1831
+ let asset = options.asset;
1832
+ let checksum = options.checksum;
1833
+ if (asset === null) {
1834
+ asset = join(scratch, name);
1835
+ checksum = join(scratch, name + ".sha256");
1836
+ const base = `https://github.com/${REPOSITORY}/releases/download/v${PRODUCT_VERSION}`;
1837
+ await downloadFile(base + "/" + name, asset);
1838
+ await downloadFile(base + "/" + name + ".sha256", checksum);
1839
+ }
1840
+ const expected = parseChecksumText(await readFile(checksum, "utf8"), basename(asset));
1841
+ if (await sha256File(asset) !== expected) fail("profile archive checksum mismatch");
1842
+ await requireRegularFileEntry(asset, "profile archive");
1843
+ const captured = join(scratch, "verified.zip");
1844
+ await copyFile(asset, captured, fsConstants.COPYFILE_EXCL);
1845
+ if (await sha256File(captured) !== expected) fail("profile archive changed during preparation");
1846
+ asset = captured;
1847
+ archiveEntries(asset);
1848
+ if (process.platform !== "win32") {
1849
+ const zipinfo = run("/usr/bin/zipinfo", ["-l", asset]);
1850
+ if (/^[lbcps][-rwx]/mu.test(zipinfo.stdout)) fail("profile archive contains non-regular entries");
1851
+ }
1852
+ const root = join(scratch, "package");
1853
+ await extractArchive(asset, root);
1854
+ await walkFiles(root);
1855
+ if (process.platform === "win32") windowsAction("seal-new", root);
1856
+ return { root, asset, cleanup: async () => rm(scratch, { recursive: true, force: true }) };
1857
+ } catch (error) {
1858
+ await rm(scratch, { recursive: true, force: true });
1859
+ throw error;
1860
+ }
1861
+ }
1862
+
1764
1863
  function showHelp() {
1765
1864
  console.log(`OpenSocrates ${PRODUCT_VERSION}
1766
1865
 
1767
1866
  Usage:
1768
- opensocrates install [--host all|antigravity|claude|codex|cursor|grok|opencode] [--asset ZIP --checksum FILE]
1769
- opensocrates status [--host all|antigravity|claude|codex|cursor|grok|opencode]
1770
- opensocrates update [--host all|antigravity|claude|codex|cursor|grok|opencode] [--asset ZIP --checksum FILE]
1771
- opensocrates remove [--host all|antigravity|claude|codex|cursor|grok|opencode] [--purge [--reset-trust]]
1772
- opensocrates verify [--host all|antigravity|claude|codex|cursor|grok|opencode] [--asset ZIP --checksum FILE]
1773
- opensocrates auto-update enable [--host all|antigravity|claude|codex|cursor|grok|opencode]
1867
+ opensocrates install [--host codex|all] [--asset ZIP --checksum FILE]
1868
+ opensocrates status [--host codex|all]
1869
+ opensocrates update [--host codex|all] [--asset ZIP --checksum FILE]
1870
+ opensocrates remove [--host codex|all] [--purge [--reset-trust]]
1871
+ opensocrates verify [--host codex|all] [--asset ZIP --checksum FILE]
1872
+ opensocrates install|update --host claude [--asset ZIP --checksum FILE]
1873
+ opensocrates install|update --host antigravity [--workspace FOLDER]
1874
+ [--asset ZIP --checksum FILE]
1875
+ opensocrates status|diagnose|disable|enable|remove --host claude|antigravity
1876
+ [--workspace FOLDER]
1877
+ opensocrates verify --host claude|antigravity|claude-chat
1878
+ [--asset ZIP --checksum FILE]
1879
+ opensocrates export --host claude-chat --output ZIP
1880
+ [--asset ZIP --checksum FILE]
1881
+ opensocrates auto-update enable [--host codex|all]
1774
1882
  [--channel stable|next] [--interval-hours ${AUTO_UPDATE_DEFAULT_INTERVAL_HOURS}]
1775
1883
  [--allow-major]
1776
1884
  opensocrates auto-update status
@@ -1780,7 +1888,13 @@ Without --asset, install, update, and verify download the v${PRODUCT_VERSION}
1780
1888
  package and checksum from GitHub Releases. The default lifecycle host is codex.
1781
1889
  With --host all, supplied host-qualified asset/checksum pairs define the exact
1782
1890
  transaction set and are never mixed with downloads for other hosts. Without
1783
- qualified assets, every ready host participates. Automatic updates are opt-in.
1891
+ qualified assets, ready Codex hosts participate. The additional profiles
1892
+ require an explicit --host; all does not activate them. Automatic updates are opt-in
1893
+ for Codex. Claude native lifecycle requires macOS arm64 and the Claude CLI.
1894
+ Antigravity installs owned rules and skills globally or in the explicit workspace
1895
+ on macOS arm64 and Windows x64. Claude Chat exports a verified portable skill ZIP;
1896
+ upload and account activation happen
1897
+ through Claude's interface. These profiles do not accept purge or reset-trust.
1784
1898
 
1785
1899
  Ordinary remove unregisters the selected host and removes installer-managed
1786
1900
  files, but may leave host-owned caches, installer state, and Codex hook trust.
@@ -1794,28 +1908,9 @@ reported as pending.
1794
1908
  }
1795
1909
 
1796
1910
  function managedPaths(host) {
1797
- const configured =
1798
- host === "antigravity"
1799
- ? process.env.ANTIGRAVITY_CONFIG_DIR
1800
- : host === "cursor"
1801
- ? process.env.CURSOR_CONFIG_DIR
1802
- : host === "grok"
1803
- ? process.env.GROK_HOME
1804
- : host === "opencode"
1805
- ? process.env.OPENCODE_CONFIG_DIR
1806
- : host === "claude"
1807
- ? process.env.CLAUDE_CONFIG_DIR
1808
- : process.env.CODEX_HOME;
1809
- const defaultHome =
1810
- host === "antigravity"
1811
- ? join(homedir(), ".gemini", "config")
1812
- : host === "cursor"
1813
- ? join(homedir(), ".cursor")
1814
- : host === "grok"
1815
- ? join(homedir(), ".grok")
1816
- : host === "opencode"
1817
- ? join(homedir(), ".config", "opencode")
1818
- : join(homedir(), host === "claude" ? ".claude" : ".codex");
1911
+ if (host !== "codex") fail("only Codex is supported");
1912
+ const configured = process.env.CODEX_HOME;
1913
+ const defaultHome = join(homedir(), ".codex");
1819
1914
  const configuredHome = resolve(configured ? configured : defaultHome);
1820
1915
  let hostHome = configuredHome;
1821
1916
  try {
@@ -1825,27 +1920,21 @@ function managedPaths(host) {
1825
1920
  // explicit, non-recursive target. Lexical resolution is the only
1826
1921
  // available normalization until then.
1827
1922
  }
1828
- const root =
1829
- host === "antigravity"
1830
- ? join(hostHome, "plugins", PLUGIN_NAME)
1831
- : host === "cursor"
1832
- ? join(hostHome, "plugins", "local", PLUGIN_NAME)
1833
- : host === "grok"
1834
- ? join(hostHome, "plugins", PLUGIN_NAME)
1835
- : host === "opencode"
1836
- ? join(hostHome, "skills", PLUGIN_NAME)
1837
- : join(hostHome, "managed-marketplaces", MARKETPLACE_NAME);
1923
+ const root = join(hostHome, "managed-marketplaces", MARKETPLACE_NAME);
1838
1924
  const layout = HOST_LAYOUTS[host];
1839
1925
  return {
1840
1926
  hostHome,
1841
1927
  root,
1842
1928
  parent: dirname(root),
1843
1929
  marker: join(root, MARKER_NAME),
1844
- marketplace: typeof layout.marketplaceRelative === "string" ? join(root, layout.marketplaceRelative) : null,
1930
+ marketplace:
1931
+ typeof layout.marketplaceRelative === "string"
1932
+ ? join(root, layout.marketplaceRelative)
1933
+ : null,
1845
1934
  plugin: join(root, layout.pluginRelative),
1846
- bridge: host === "opencode" ? join(hostHome, "plugins", "opensocrates.js") : null,
1847
- bridgeMarker: host === "opencode" ? join(hostHome, "plugins", OPENCODE_BRIDGE_MARKER) : null,
1848
- bridgeParent: host === "opencode" ? join(hostHome, "plugins") : null,
1935
+ bridge: null,
1936
+ bridgeMarker: null,
1937
+ bridgeParent: null,
1849
1938
  };
1850
1939
  }
1851
1940
 
@@ -1854,32 +1943,19 @@ export function purgePathsFor(host) {
1854
1943
  fail(`unsupported host ${JSON.stringify(host)}`);
1855
1944
  }
1856
1945
  const paths = managedPaths(host);
1857
- const cacheRoot = ["claude", "codex"].includes(host)
1858
- ? join(paths.hostHome, "plugins", "cache", MARKETPLACE_NAME, PLUGIN_NAME)
1859
- : null;
1946
+ const cacheRoot = join(paths.hostHome, "plugins", "cache", MARKETPLACE_NAME, PLUGIN_NAME);
1860
1947
  return {
1861
1948
  ...paths,
1862
1949
  cacheRoot,
1863
1950
  cacheMarketplaceRoot: cacheRoot === null ? null : dirname(cacheRoot),
1864
- pluginData:
1865
- host === "claude"
1866
- ? [
1867
- join(paths.hostHome, "plugins", "data", "opensocrates-inline"),
1868
- join(paths.hostHome, "plugins", "data", "opensocrates-opensocrates"),
1869
- ]
1870
- : [],
1951
+ pluginData: [],
1871
1952
  };
1872
1953
  }
1873
1954
 
1874
1955
  // Directory that holds staging trees and rollback backups for one host.
1875
1956
  //
1876
- // Grok scans every directory below ~/.grok/plugins, including dot-prefixed
1877
- // ones, so a staging tree or rollback backup left there would be discovered as
1878
- // a second OpenSocrates plugin. An interrupted install must not be able to
1879
- // leave a shadow copy inside Grok's plugin discovery root, so both transient
1880
- // directories live in the Grok home instead, next to the scanned directory.
1881
1957
  function transientParent(host, paths) {
1882
- return host === "grok" ? paths.hostHome : paths.parent;
1958
+ return paths.parent;
1883
1959
  }
1884
1960
 
1885
1961
  export function transientPathsFor(host) {
@@ -1891,34 +1967,40 @@ function codexBinary() {
1891
1967
  return process.env.CODEX_BIN || "codex";
1892
1968
  }
1893
1969
 
1894
- function claudeBinary() {
1895
- return process.env.CLAUDE_BIN || "claude";
1896
- }
1897
-
1898
- function cursorBinary() {
1899
- return process.env.CURSOR_BIN || "cursor";
1900
- }
1901
-
1902
- function cursorAppPaths() {
1903
- return ["/Applications/Cursor.app", join(homedir(), "Applications", "Cursor.app")];
1904
- }
1905
-
1906
- function antigravityBinary() {
1907
- return process.env.AGY_BIN || "agy";
1908
- }
1909
-
1910
- function grokBinary() {
1911
- return process.env.GROK_BIN || "grok";
1912
- }
1913
-
1914
- function opencodeBinary() {
1915
- return process.env.OPENCODE_BIN || "opencode";
1970
+ function windowsAction(action, target, destination = "") {
1971
+ const result = spawnSync(
1972
+ "powershell.exe",
1973
+ [
1974
+ "-NoProfile",
1975
+ "-NonInteractive",
1976
+ "-ExecutionPolicy",
1977
+ "Bypass",
1978
+ "-File",
1979
+ fileURLToPath(new URL("./windows.ps1", import.meta.url)),
1980
+ "-Action",
1981
+ action,
1982
+ ],
1983
+ {
1984
+ encoding: "utf8",
1985
+ windowsHide: true,
1986
+ maxBuffer: 16 * 1024 * 1024,
1987
+ env: {
1988
+ ...process.env,
1989
+ OPENSOCRATES_WINDOWS_PATH: target,
1990
+ OPENSOCRATES_WINDOWS_DESTINATION: destination,
1991
+ },
1992
+ },
1993
+ );
1994
+ if (result.error || result.status !== 0)
1995
+ fail(`Windows ${action} failed: ${result.error?.message || result.stderr?.trim()}`);
1996
+ return result.stdout;
1916
1997
  }
1917
1998
 
1918
1999
  function run(command, args, { allowFailure = false } = {}) {
1919
2000
  const result = spawnSync(command, args, {
1920
2001
  encoding: "utf8",
1921
2002
  env: process.env,
2003
+ windowsHide: true,
1922
2004
  maxBuffer: 16 * 1024 * 1024,
1923
2005
  });
1924
2006
  if (result.error) {
@@ -1958,139 +2040,7 @@ function runCodexJson(args) {
1958
2040
  return payload;
1959
2041
  }
1960
2042
 
1961
- function runClaudeJson(args) {
1962
- return runJson(claudeBinary(), args, "Claude Code");
1963
- }
1964
-
1965
- function runGrokJson(args) {
1966
- return runJson(grokBinary(), args, "Grok Build");
1967
- }
1968
-
1969
- function claudeListEntries(payload, wrapper, label) {
1970
- if (Array.isArray(payload)) {
1971
- return payload;
1972
- }
1973
- if (
1974
- payload !== null &&
1975
- typeof payload === "object" &&
1976
- !Array.isArray(payload) &&
1977
- Object.keys(payload).length === 1 &&
1978
- Array.isArray(payload[wrapper])
1979
- ) {
1980
- return payload[wrapper];
1981
- }
1982
- fail(`Claude Code ${label} returned an unexpected schema`);
1983
- }
1984
-
1985
- function requireUniqueClaudeEntries(entries, identity, label) {
1986
- const seen = new Set();
1987
- for (const entry of entries) {
1988
- if (
1989
- entry === null ||
1990
- typeof entry !== "object" ||
1991
- Array.isArray(entry) ||
1992
- typeof entry[identity] !== "string" ||
1993
- entry[identity].trim().length === 0
1994
- ) {
1995
- fail(`Claude Code ${label} returned a malformed entry`);
1996
- }
1997
- if (seen.has(entry[identity])) {
1998
- fail(`Claude Code ${label} returned duplicate entries for ${entry[identity]}`);
1999
- }
2000
- seen.add(entry[identity]);
2001
- }
2002
- return entries;
2003
- }
2004
-
2005
- function claudeMarketplaceEntries(payload) {
2006
- return requireUniqueClaudeEntries(
2007
- claudeListEntries(payload, "marketplaces", "marketplace list"),
2008
- "name",
2009
- "marketplace list",
2010
- );
2011
- }
2012
-
2013
- function claudePluginEntries(payload) {
2014
- return requireUniqueClaudeEntries(claudeListEntries(payload, "plugins", "plugin list"), "id", "plugin list");
2015
- }
2016
-
2017
- function versionAtLeast(value, minimum) {
2018
- const match = String(value).match(/(\d+)\.(\d+)\.(\d+)/u);
2019
- if (!match) {
2020
- return false;
2021
- }
2022
- const current = match.slice(1).map(Number);
2023
- for (let index = 0; index < minimum.length; index += 1) {
2024
- if (current[index] > minimum[index]) return true;
2025
- if (current[index] < minimum[index]) return false;
2026
- }
2027
- return true;
2028
- }
2029
-
2030
2043
  function requireHostCli(host, { authenticated = false } = {}) {
2031
- if (host === "grok") {
2032
- const result = run(grokBinary(), ["--version"]);
2033
- if (!versionAtLeast(result.stdout, [1, 0, 3])) {
2034
- fail(`Grok Build 1.0.3 or later is required; got ${result.stdout.trim()}`);
2035
- }
2036
- return;
2037
- }
2038
- if (host === "cursor") {
2039
- if (process.env.CURSOR_CONFIG_DIR) return;
2040
- if (process.env.CURSOR_BIN) {
2041
- const result = run(cursorBinary(), ["--version"]);
2042
- if (!versionAtLeast(result.stdout, [2, 5, 0])) {
2043
- fail(`Cursor 2.5.0 or later is required; got ${result.stdout.trim()}`);
2044
- }
2045
- return;
2046
- }
2047
- const app = cursorAppPaths().find((candidate) => existsSync(candidate));
2048
- if (app) {
2049
- const result = run(
2050
- "/usr/bin/plutil",
2051
- ["-extract", "CFBundleShortVersionString", "raw", join(app, "Contents", "Info.plist")],
2052
- { allowFailure: true },
2053
- );
2054
- if (result.status !== 0 || !versionAtLeast(result.stdout, [2, 5, 0])) {
2055
- fail("Cursor 2.5.0 or later is required for Agent Plugin support");
2056
- }
2057
- return;
2058
- }
2059
- const result = run(cursorBinary(), ["--version"]);
2060
- if (!versionAtLeast(result.stdout, [2, 5, 0])) {
2061
- fail(`Cursor 2.5.0 or later is required; got ${result.stdout.trim()}`);
2062
- }
2063
- return;
2064
- }
2065
- if (host === "antigravity") {
2066
- if (process.env.ANTIGRAVITY_CONFIG_DIR) return;
2067
- run(antigravityBinary(), ["--version"]);
2068
- return;
2069
- }
2070
- if (host === "opencode") {
2071
- // OPENCODE_CONFIG_DIR only redirects where the managed files are written;
2072
- // it does not waive the supported-host gate. Unlike the content-only
2073
- // file-drop hosts above, OpenCode receives an executing bridge bound to the
2074
- // stable chat.message contract, and the shipped package declares
2075
- // minimum_opencode_version 1.18.18 with a fail-closed <2.0.0 ceiling.
2076
- // Installing that bridge without confirming the host version would ship
2077
- // executable code against an unverified plugin API.
2078
- const result = run(opencodeBinary(), ["--version"]);
2079
- if (!versionAtLeast(result.stdout, [1, 18, 18]) || versionAtLeast(result.stdout, [2, 0, 0])) {
2080
- fail(`OpenCode >=1.18.18 and <2.0.0 is required; got ${result.stdout.trim()}`);
2081
- }
2082
- return;
2083
- }
2084
- if (host === "claude") {
2085
- const result = run(claudeBinary(), ["--version"]);
2086
- if (!versionAtLeast(result.stdout, [2, 1, 205])) {
2087
- fail(`Claude Code 2.1.205 or later is required for structured selector output; got ${result.stdout.trim()}`);
2088
- }
2089
- if (authenticated) {
2090
- run(claudeBinary(), ["auth", "status"]);
2091
- }
2092
- return;
2093
- }
2094
2044
  run(codexBinary(), ["--version"]);
2095
2045
  if (authenticated) {
2096
2046
  run(codexBinary(), ["login", "status"]);
@@ -2098,17 +2048,8 @@ function requireHostCli(host, { authenticated = false } = {}) {
2098
2048
  }
2099
2049
 
2100
2050
  function marketplaceEntries(host) {
2101
- if (FILE_DROP_HOSTS.includes(host) || host === "opencode") {
2102
- const paths = managedPaths(host);
2103
- const present =
2104
- existsSync(paths.root) || (host === "opencode" && (existsSync(paths.bridge) || existsSync(paths.bridgeMarker)));
2105
- return present ? [{ name: MARKETPLACE_NAME, root: paths.root }] : [];
2106
- }
2107
- const payload =
2108
- host === "claude"
2109
- ? runClaudeJson(["plugin", "marketplace", "list", "--json"])
2110
- : runCodexJson(["plugin", "marketplace", "list", "--json"]).marketplaces;
2111
- const entries = host === "claude" ? claudeMarketplaceEntries(payload) : payload;
2051
+ const payload = runCodexJson(["plugin", "marketplace", "list", "--json"]).marketplaces;
2052
+ const entries = payload;
2112
2053
  if (!Array.isArray(entries)) {
2113
2054
  fail(`${host} marketplace list returned an unexpected schema`);
2114
2055
  }
@@ -2124,143 +2065,15 @@ function marketplaceEntry(host) {
2124
2065
  return matches[0] ?? null;
2125
2066
  }
2126
2067
 
2127
- // Read Grok Build's machine-readable plugin state.
2128
- //
2129
- // The managed Grok files are host-independent content, so inspecting or
2130
- // removing what the installer owns must not depend on a runnable `grok`
2131
- // command: a user who uninstalls Grok Build still has to be able to clean up.
2132
- // Transactional callers pass requireHostState so that install and update keep
2133
- // confirming activation against the host itself.
2134
- function grokInspection(requireHostState) {
2135
- if (requireHostState) return runGrokJson(["inspect", "--json"]);
2136
- const result = run(grokBinary(), ["inspect", "--json"], { allowFailure: true });
2137
- if (result.error || result.status !== 0) {
2138
- const detail = result.error?.message || result.stderr?.trim() || `exit ${result.status}`;
2139
- console.warn(
2140
- `warning: could not read Grok Build plugin state (${detail}); ` +
2141
- "reporting the managed files only",
2142
- );
2143
- return null;
2144
- }
2145
- try {
2146
- const payload = JSON.parse(result.stdout);
2147
- if (payload === null || typeof payload !== "object") {
2148
- fail("Grok Build returned a non-container JSON value");
2149
- }
2150
- return payload;
2151
- } catch (error) {
2152
- if (error instanceof InstallerError) throw error;
2153
- fail(`Grok Build returned invalid JSON for inspect --json: ${error.message}`);
2154
- }
2155
- }
2156
-
2157
2068
  function pluginState(host, { requireHostState = false } = {}) {
2158
- if (FILE_DROP_HOSTS.includes(host)) {
2159
- const paths = managedPaths(host);
2160
- if (!existsSync(paths.root)) return { kind: "missing", version: null };
2161
- let manifest;
2162
- try {
2163
- manifest = JSON.parse(readFileSync(join(paths.root, "plugin.json"), "utf8"));
2164
- } catch (error) {
2165
- fail(`${host} plugin manifest is unreadable: ${error.message}`);
2166
- }
2167
- if (
2168
- manifest === null ||
2169
- typeof manifest !== "object" ||
2170
- Array.isArray(manifest) ||
2171
- (host === "cursor" &&
2172
- manifest.$schema !== "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json") ||
2173
- (host === "grok" && manifest.skills !== "./skills") ||
2174
- manifest.name !== PLUGIN_NAME ||
2175
- typeof manifest.version !== "string" ||
2176
- manifest.version.trim().length === 0
2177
- ) {
2178
- fail(`${host} reported an invalid OpenSocrates plugin manifest`);
2179
- }
2180
- if (host !== "grok") return { kind: "installed", version: manifest.version };
2181
- const payload = grokInspection(requireHostState);
2182
- if (payload === null) return { kind: "installed", version: manifest.version };
2183
- if (!Array.isArray(payload.plugins)) {
2184
- fail("Grok Build inspect returned an unexpected plugin schema");
2185
- }
2186
- const matches = payload.plugins.filter((entry) => entry?.name === PLUGIN_NAME);
2187
- if (matches.length !== 1) {
2188
- fail(
2189
- matches.length === 0
2190
- ? "Grok Build did not discover the managed OpenSocrates plugin"
2191
- : "Grok Build reported duplicate OpenSocrates plugins",
2192
- );
2193
- }
2194
- const entry = matches[0];
2195
- if (
2196
- typeof entry.enabled !== "boolean" ||
2197
- typeof entry.path !== "string" ||
2198
- entry.provides?.skills !== 1 ||
2199
- entry.provides?.agents !== 0 ||
2200
- entry.provides?.hooks !== false ||
2201
- entry.provides?.mcpServers !== 0
2202
- ) {
2203
- fail("Grok Build reported an invalid OpenSocrates plugin state");
2204
- }
2205
- let observedPath = resolve(entry.path);
2206
- try {
2207
- observedPath = realpathSync(observedPath);
2208
- } catch {
2209
- // The managed root exists here, so a missing reported path is invalid below.
2210
- }
2211
- let expectedPath = resolve(paths.root);
2212
- try {
2213
- expectedPath = realpathSync(expectedPath);
2214
- } catch {
2215
- // Ownership checks report a more specific error for a missing root.
2216
- }
2217
- if (observedPath !== expectedPath) {
2218
- fail(`Grok Build resolved OpenSocrates from an unmanaged location: ${observedPath}`);
2219
- }
2220
- return { kind: entry.enabled ? "installed" : "disabled", version: manifest.version };
2221
- }
2222
- if (host === "opencode") {
2223
- const paths = managedPaths(host);
2224
- if (!existsSync(paths.root) && !existsSync(paths.bridge) && !existsSync(paths.bridgeMarker)) {
2225
- return { kind: "missing", version: null };
2226
- }
2227
- let manifest;
2228
- try {
2229
- manifest = JSON.parse(readFileSync(join(paths.root, ".opensocrates-package", "opencode-plugin.json"), "utf8"));
2230
- } catch (error) {
2231
- fail(`opencode installed manifest is unreadable: ${error.message}`);
2232
- }
2233
- if (
2234
- manifest === null ||
2235
- typeof manifest !== "object" ||
2236
- Array.isArray(manifest) ||
2237
- manifest.name !== PLUGIN_NAME ||
2238
- typeof manifest.version !== "string" ||
2239
- manifest.version.trim().length === 0
2240
- ) {
2241
- fail("opencode reported an invalid OpenSocrates installation manifest");
2242
- }
2243
- return { kind: "installed", version: manifest.version };
2244
- }
2245
- if (host === "claude") {
2246
- const installed = claudePluginEntries(runClaudeJson(["plugin", "list", "--json"]));
2247
- const matches = installed.filter((entry) => entry?.id === PLUGIN_ID);
2248
- if (matches.length === 0) return { kind: "missing", version: null };
2249
- const entry = matches[0];
2250
- if (
2251
- (entry.version !== undefined &&
2252
- entry.version !== null &&
2253
- (typeof entry.version !== "string" || entry.version.trim().length === 0)) ||
2254
- typeof entry.enabled !== "boolean"
2255
- ) {
2256
- fail(`Claude Code reported an invalid state for ${PLUGIN_ID}`);
2257
- }
2258
- return {
2259
- kind: entry.enabled ? "installed" : "disabled",
2260
- version: entry.version ?? null,
2261
- };
2262
- }
2263
- const payload = runCodexJson(["plugin", "list", "--marketplace", MARKETPLACE_NAME, "--available", "--json"]);
2069
+ const payload = runCodexJson([
2070
+ "plugin",
2071
+ "list",
2072
+ "--marketplace",
2073
+ MARKETPLACE_NAME,
2074
+ "--available",
2075
+ "--json",
2076
+ ]);
2264
2077
  const installed = Array.isArray(payload.installed) ? payload.installed : null;
2265
2078
  const available = Array.isArray(payload.available) ? payload.available : null;
2266
2079
  if (installed === null || available === null) {
@@ -2286,51 +2099,10 @@ function pluginState(host, { requireHostState = false } = {}) {
2286
2099
  return { kind: "missing", version: null };
2287
2100
  }
2288
2101
 
2289
- function detectLegacyClaudeInstallation() {
2290
- const marketplaces = marketplaceEntries("claude").filter(
2291
- (entry) =>
2292
- typeof entry?.name === "string" &&
2293
- entry.name !== MARKETPLACE_NAME &&
2294
- entry.name.toLowerCase() === MARKETPLACE_NAME,
2295
- );
2296
- const plugins = claudePluginEntries(runClaudeJson(["plugin", "list", "--json"]));
2297
- const legacyPlugins = plugins.filter((entry) => entry.id !== PLUGIN_ID && entry.id.toLowerCase() === PLUGIN_ID);
2298
- const names = marketplaces.map((entry) => entry.name).join(", ") || "OpenSocrates";
2299
- return {
2300
- found: marketplaces.length > 0 || legacyPlugins.length > 0,
2301
- names,
2302
- };
2303
- }
2304
-
2305
2102
  // Install and update write into the managed root, so a case-variant pre-1.0
2306
2103
  // registration must be resolved by the user first. Status and remove are
2307
2104
  // diagnostic and scoped to the root this installer owns, so they only warn:
2308
2105
  // blocking them would strand a user who needs to inspect or back out.
2309
- function requireNoLegacyClaudeInstallation() {
2310
- const legacy = detectLegacyClaudeInstallation();
2311
- if (legacy.found) {
2312
- fail(
2313
- `a legacy Claude marketplace (${legacy.names}) is installed; OpenSocrates will not remove it automatically. ` +
2314
- "Uninstall its plugin and marketplace explicitly, then rerun this command. See the README migration section.",
2315
- );
2316
- }
2317
- }
2318
-
2319
- function warnLegacyClaudeInstallation() {
2320
- let legacy;
2321
- try {
2322
- legacy = detectLegacyClaudeInstallation();
2323
- } catch {
2324
- return;
2325
- }
2326
- if (legacy.found) {
2327
- console.warn(
2328
- `warning: a legacy Claude marketplace (${legacy.names}) is also registered. ` +
2329
- "OpenSocrates never removes it automatically; this command only affects the root it owns. " +
2330
- "See the README migration section.",
2331
- );
2332
- }
2333
- }
2334
2106
 
2335
2107
  async function readJsonObject(target) {
2336
2108
  let payload;
@@ -2366,105 +2138,26 @@ async function sha256File(target) {
2366
2138
  return hash.digest("hex");
2367
2139
  }
2368
2140
 
2369
- function openCodeSidecar(bridgeSha256) {
2370
- return {
2371
- ...OPENCODE_MARKER,
2372
- version: PRODUCT_VERSION,
2373
- bridgeSha256,
2374
- };
2375
- }
2376
-
2377
- async function ensureSafeDirectory(target, label) {
2378
- if (await exists(target)) {
2379
- const info = await lstat(target);
2380
- if (!info.isDirectory() || info.isSymbolicLink()) {
2381
- fail(`refusing to use an unsafe ${label} directory: ${target}`);
2382
- }
2383
- return;
2141
+ async function downloadFile(url, destination) {
2142
+ const response = await fetch(url, {
2143
+ headers: {
2144
+ accept: "application/octet-stream",
2145
+ "user-agent": `OpenSocrates-installer/${PRODUCT_VERSION}`,
2146
+ },
2147
+ redirect: "follow",
2148
+ signal: AbortSignal.timeout(15 * 60 * 1000),
2149
+ });
2150
+ if (!response.ok || response.body === null) {
2151
+ fail(`download failed (${response.status}) for ${url}`);
2384
2152
  }
2385
- await mkdir(target, { recursive: true, mode: 0o700 });
2386
- const info = await lstat(target);
2387
- if (!info.isDirectory() || info.isSymbolicLink()) {
2388
- fail(`refusing to use an unsafe ${label} directory: ${target}`);
2389
- }
2390
- }
2391
-
2392
- async function ensureOpenCodeDirectories(paths) {
2393
- await ensureSafeDirectory(paths.hostHome, "OpenCode configuration");
2394
- await ensureSafeDirectory(paths.parent, "OpenCode skills");
2395
- await ensureSafeDirectory(paths.bridgeParent, "OpenCode plugins");
2396
- }
2397
-
2398
- async function verifyOpenCodeInstallation(paths) {
2399
- await requireOwnedRoot(paths.root, "opencode");
2400
- for (const [target, label] of [
2401
- [paths.bridge, "plugin bridge"],
2402
- [paths.bridgeMarker, "plugin ownership sidecar"],
2403
- ]) {
2404
- if (!(await exists(target))) fail(`OpenCode ${label} is missing: ${target}`);
2405
- const info = await lstat(target);
2406
- if (!info.isFile() || info.isSymbolicLink()) {
2407
- fail(`OpenCode ${label} is not an owned regular file: ${target}`);
2408
- }
2409
- }
2410
- const sidecar = await readJsonObject(paths.bridgeMarker);
2411
- const sidecarMarker = Object.fromEntries(Object.keys(OPENCODE_MARKER).map((key) => [key, sidecar[key]]));
2412
- if (
2413
- !markerMatches(sidecarMarker, "opencode") ||
2414
- sidecar.version !== PRODUCT_VERSION ||
2415
- typeof sidecar.bridgeSha256 !== "string" ||
2416
- sidecar.bridgeSha256 !== (await sha256File(paths.bridge))
2417
- ) {
2418
- fail("OpenCode plugin bridge has an invalid ownership sidecar or checksum");
2419
- }
2420
- const manifest = await readJsonObject(join(paths.root, OPENCODE_INSTALL_MANIFEST));
2421
- if (
2422
- manifest.schema !== "opensocrates.opencode-installation/1.0.0" ||
2423
- manifest.version !== PRODUCT_VERSION ||
2424
- manifest.bridgeSha256 !== sidecar.bridgeSha256 ||
2425
- manifest.files === null ||
2426
- typeof manifest.files !== "object" ||
2427
- Array.isArray(manifest.files)
2428
- ) {
2429
- fail("OpenCode installation inventory is invalid");
2430
- }
2431
- const actualFiles = new Set(await walkFiles(paths.root));
2432
- actualFiles.delete(OPENCODE_INSTALL_MANIFEST);
2433
- const declaredFiles = Object.keys(manifest.files);
2434
- if (
2435
- actualFiles.size !== declaredFiles.length ||
2436
- declaredFiles.some((item) => !isSafeArchivePath(item) || !actualFiles.has(item))
2437
- ) {
2438
- fail("OpenCode installation inventory does not cover the complete managed skill");
2439
- }
2440
- for (const item of declaredFiles) {
2441
- if (manifest.files[item] !== (await sha256File(join(paths.root, ...item.split("/"))))) {
2442
- fail(`OpenCode installed file checksum mismatch for ${item}`);
2443
- }
2444
- }
2445
- return manifest;
2446
- }
2447
-
2448
- async function downloadFile(url, destination) {
2449
- const response = await fetch(url, {
2450
- headers: {
2451
- accept: "application/octet-stream",
2452
- "user-agent": `OpenSocrates-installer/${PRODUCT_VERSION}`,
2453
- },
2454
- redirect: "follow",
2455
- signal: AbortSignal.timeout(15 * 60 * 1000),
2456
- });
2457
- if (!response.ok || response.body === null) {
2458
- fail(`download failed (${response.status}) for ${url}`);
2459
- }
2460
- const contentLength = Number(response.headers.get("content-length") ?? "0");
2461
- if (Number.isFinite(contentLength) && contentLength > MAX_DOWNLOAD_BYTES) {
2462
- fail(`download exceeds the ${MAX_DOWNLOAD_BYTES}-byte safety limit`);
2463
- }
2464
- await pipeline(Readable.fromWeb(response.body), createWriteStream(destination, { mode: 0o600 }));
2465
- const downloaded = (await stat(destination)).size;
2466
- if (downloaded === 0 || downloaded > MAX_DOWNLOAD_BYTES) {
2467
- fail(`downloaded file has an invalid size: ${downloaded} bytes`);
2153
+ const contentLength = Number(response.headers.get("content-length") ?? "0");
2154
+ if (Number.isFinite(contentLength) && contentLength > MAX_DOWNLOAD_BYTES) {
2155
+ fail(`download exceeds the ${MAX_DOWNLOAD_BYTES}-byte safety limit`);
2156
+ }
2157
+ await pipeline(Readable.fromWeb(response.body), createWriteStream(destination, { mode: 0o600 }));
2158
+ const downloaded = (await stat(destination)).size;
2159
+ if (downloaded === 0 || downloaded > MAX_DOWNLOAD_BYTES) {
2160
+ fail(`downloaded file has an invalid size: ${downloaded} bytes`);
2468
2161
  }
2469
2162
  }
2470
2163
 
@@ -2509,8 +2202,10 @@ async function verifyOuterChecksum(asset, checksum, host) {
2509
2202
 
2510
2203
  function archiveEntries(asset) {
2511
2204
  const unzip = process.platform === "darwin" ? "/usr/bin/unzip" : "unzip";
2512
- const result = run(unzip, ["-Z1", asset]);
2513
- const entries = result.stdout.split(/\r?\n/u).filter(Boolean);
2205
+ const entries =
2206
+ process.platform === "win32"
2207
+ ? JSON.parse(windowsAction("entries", asset))
2208
+ : run(unzip, ["-Z1", asset]).stdout.split(/\r?\n/u).filter(Boolean);
2514
2209
  if (entries.length === 0 || entries.length > MAX_ARCHIVE_ENTRIES) {
2515
2210
  fail(`archive contains an invalid number of entries: ${entries.length}`);
2516
2211
  }
@@ -2531,7 +2226,8 @@ async function extractArchive(asset, destination) {
2531
2226
  archiveEntries(asset);
2532
2227
  await mkdir(destination, { recursive: true, mode: 0o700 });
2533
2228
  const unzip = process.platform === "darwin" ? "/usr/bin/unzip" : "unzip";
2534
- run(unzip, ["-q", asset, "-d", destination]);
2229
+ if (process.platform === "win32") windowsAction("extract", asset, destination);
2230
+ else run(unzip, ["-q", asset, "-d", destination]);
2535
2231
  }
2536
2232
 
2537
2233
  async function walkFiles(root, current = root, output = []) {
@@ -2580,13 +2276,23 @@ async function verifyPackageChecksums(pluginRoot) {
2580
2276
  fail(`package checksum mismatch for ${item}`);
2581
2277
  }
2582
2278
  }
2583
- const actualFiles = new Set((await walkFiles(pluginRoot)).filter((item) => item !== "checksums.sha256"));
2279
+ const actualFiles = new Set(
2280
+ (await walkFiles(pluginRoot)).filter((item) => item !== "checksums.sha256"),
2281
+ );
2584
2282
  if (actualFiles.size !== declared.size || [...actualFiles].some((item) => !declared.has(item))) {
2585
2283
  fail("package checksum manifest does not cover the complete archive");
2586
2284
  }
2587
2285
  return declared.size;
2588
2286
  }
2589
2287
 
2288
+ function hasExactStringEntries(value, expected) {
2289
+ return (
2290
+ Array.isArray(value) &&
2291
+ value.length === expected.length &&
2292
+ value.every((item, index) => item === expected[index])
2293
+ );
2294
+ }
2295
+
2590
2296
  async function verifyExtractedPackage(pluginRoot, host) {
2591
2297
  const manifest = await readJsonObject(join(pluginRoot, HOST_LAYOUTS[host].manifestRelative));
2592
2298
  if (manifest.name !== PLUGIN_NAME || manifest.version !== PRODUCT_VERSION) {
@@ -2604,36 +2310,56 @@ async function verifyExtractedPackage(pluginRoot, host) {
2604
2310
  fail("package release manifest does not match this installer");
2605
2311
  }
2606
2312
  if (HOST_LAYOUTS[host].requiresRuntime) {
2607
- const runtime = join(pluginRoot, "runtime", "darwin-arm64", "opensocrates-runtime", "opensocrates-runtime");
2608
- const runtimeInfo = await stat(runtime);
2609
- if (!runtimeInfo.isFile() || (runtimeInfo.mode & 0o111) === 0) {
2610
- fail("package is missing the executable darwin-arm64 runtime");
2313
+ const target = process.platform === "win32" ? "windows-x64" : "darwin-arm64";
2314
+ const runtimeRoot = join(pluginRoot, "runtime");
2315
+ const executable =
2316
+ process.platform === "win32" ? "opensocrates-runtime.exe" : "opensocrates-runtime";
2317
+ const runtime = join(runtimeRoot, target, "opensocrates-runtime", executable);
2318
+ if (
2319
+ !hasExactStringEntries(release.runtime_targets, [target]) ||
2320
+ !hasExactStringEntries(release.release_targets, [target])
2321
+ ) {
2322
+ fail(`package target metadata does not declare only ${target}`);
2323
+ }
2324
+ let packagedTargets;
2325
+ try {
2326
+ packagedTargets = await readdir(runtimeRoot, { withFileTypes: true });
2327
+ } catch {
2328
+ fail(`package runtime layout cannot be inspected for ${target}`);
2329
+ }
2330
+ if (
2331
+ packagedTargets.length !== 1 ||
2332
+ packagedTargets[0].name !== target ||
2333
+ !packagedTargets[0].isDirectory() ||
2334
+ packagedTargets[0].isSymbolicLink()
2335
+ ) {
2336
+ fail(`package runtime layout does not contain only ${target}`);
2337
+ }
2338
+ let runtimeInfo;
2339
+ try {
2340
+ runtimeInfo = await stat(runtime);
2341
+ } catch {
2342
+ fail(`package runtime layout is missing the executable ${target} runtime`);
2343
+ }
2344
+ if (
2345
+ !runtimeInfo.isFile() ||
2346
+ (process.platform !== "win32" && (runtimeInfo.mode & 0o111) === 0)
2347
+ ) {
2348
+ fail(`package runtime layout is missing the executable ${target} runtime`);
2611
2349
  }
2612
2350
  } else if (
2613
- release.launchers?.length !== 0 ||
2614
- release.runtime_targets?.length !== 0 ||
2351
+ !hasExactStringEntries(release.release_targets, []) ||
2352
+ !hasExactStringEntries(release.launchers, []) ||
2353
+ !hasExactStringEntries(release.runtime_targets, []) ||
2615
2354
  (await exists(join(pluginRoot, "runtime"))) ||
2616
2355
  (await exists(join(pluginRoot, "hooks"))) ||
2617
2356
  (await exists(join(pluginRoot, "bin"))) ||
2618
- ((host === "cursor" || host === "grok") && (await exists(join(pluginRoot, "mcp.json")))) ||
2619
- (host === "grok" &&
2620
- ((await exists(join(pluginRoot, "commands"))) ||
2621
- (await exists(join(pluginRoot, "agents"))) ||
2622
- (await exists(join(pluginRoot, ".mcp.json")))))
2357
+ false ||
2358
+ false
2623
2359
  ) {
2624
2360
  fail(`${host} package contains an unexpected native runtime or launcher surface`);
2625
2361
  }
2626
- if (host === "opencode") {
2627
- if (
2628
- manifest.minimum_opencode_version !== "1.18.18" ||
2629
- manifest.stable_plugin_hook !== "chat.message" ||
2630
- manifest.beta_v2_api !== false ||
2631
- !(await exists(join(pluginRoot, "plugins", "opensocrates.js"))) ||
2632
- !(await exists(join(pluginRoot, "skills", "opensocrates", "SKILL.md")))
2633
- ) {
2634
- fail("OpenCode package does not match the verified stable bridge contract");
2635
- }
2636
- }
2362
+
2637
2363
  return verifyPackageChecksums(pluginRoot);
2638
2364
  }
2639
2365
 
@@ -2725,7 +2451,8 @@ async function packageIdentityForPurge(pluginRoot, host, { allowedExtra = () =>
2725
2451
  fail(`OpenSocrates ${host} payload has an unsafe checksum path`);
2726
2452
  }
2727
2453
  const target = join(pluginRoot, ...item.split("/"));
2728
- if (!(await entryExists(target))) fail(`OpenSocrates ${host} payload is missing a declared file`);
2454
+ if (!(await entryExists(target)))
2455
+ fail(`OpenSocrates ${host} payload is missing a declared file`);
2729
2456
  const info = await lstat(target);
2730
2457
  if (!info.isFile() || info.isSymbolicLink()) {
2731
2458
  fail(`OpenSocrates ${host} payload contains an unsafe declared file`);
@@ -2788,7 +2515,11 @@ async function purgeHostCache(host, paths) {
2788
2515
  if (!(await entryExists(paths.cacheRoot))) {
2789
2516
  let removedEmptyMarketplace = false;
2790
2517
  if (await entryExists(paths.cacheMarketplaceRoot)) {
2791
- await requireSafePathBelow(paths.hostHome, paths.cacheMarketplaceRoot, `${host} cache marketplace`);
2518
+ await requireSafePathBelow(
2519
+ paths.hostHome,
2520
+ paths.cacheMarketplaceRoot,
2521
+ `${host} cache marketplace`,
2522
+ );
2792
2523
  const info = await lstat(paths.cacheMarketplaceRoot);
2793
2524
  if (!info.isDirectory() || info.isSymbolicLink()) {
2794
2525
  fail(`refusing an unsafe ${host} OpenSocrates cache marketplace`);
@@ -2860,90 +2591,30 @@ async function purgeHostCache(host, paths) {
2860
2591
  };
2861
2592
  }
2862
2593
 
2863
- async function purgeClaudePluginData(paths) {
2864
- const results = [];
2865
- for (const target of paths.pluginData) {
2866
- if (!(await entryExists(target))) {
2867
- results.push({ component: "plugin-data", status: "absent", path: target });
2868
- continue;
2869
- }
2870
- await requireSafePathBelow(paths.hostHome, target, "Claude OpenSocrates plugin data");
2871
- const info = await lstat(target);
2872
- if (!info.isDirectory() || info.isSymbolicLink()) {
2873
- fail(`refusing an unsafe Claude OpenSocrates plugin-data path: ${target}`);
2874
- }
2875
- if ((await readdir(target)).length !== 0) {
2876
- results.push({
2877
- component: "plugin-data",
2878
- status: "pending",
2879
- path: target,
2880
- detail: "nonempty-unverified-data",
2881
- });
2882
- continue;
2883
- }
2884
- await rmdir(target);
2885
- results.push({ component: "plugin-data", status: "removed", path: target });
2886
- }
2887
- return results;
2888
- }
2889
-
2890
- async function verifyOpenCodePurgeRoot(root) {
2891
- await requireOwnedRoot(root, "opencode");
2892
- const manifestPath = join(root, OPENCODE_INSTALL_MANIFEST);
2893
- await requireRegularFileEntry(manifestPath, "OpenCode installation inventory");
2894
- const manifest = await readJsonObject(manifestPath);
2895
- const manifestKeys = Object.keys(manifest).sort();
2896
- if (
2897
- !jsonDeepEqual(manifestKeys, ["bridgeSha256", "files", "schema", "version"]) ||
2898
- manifest.schema !== "opensocrates.opencode-installation/1.0.0" ||
2899
- typeof manifest.version !== "string" ||
2900
- manifest.version.trim().length === 0 ||
2901
- !/^[a-f0-9]{64}$/u.test(manifest.bridgeSha256) ||
2902
- manifest.files === null ||
2903
- typeof manifest.files !== "object" ||
2904
- Array.isArray(manifest.files)
2905
- ) {
2906
- fail("OpenCode installation inventory is invalid");
2907
- }
2908
- const declared = Object.keys(manifest.files);
2909
- const actual = new Set(await walkFiles(root));
2910
- actual.delete(OPENCODE_INSTALL_MANIFEST);
2911
- if (
2912
- actual.size !== declared.length ||
2913
- declared.some((item) => !isSafeArchivePath(item) || !actual.has(item) || !/^[a-f0-9]{64}$/u.test(manifest.files[item]))
2914
- ) {
2915
- fail("OpenCode installation inventory does not cover the complete managed skill");
2916
- }
2917
- for (const item of declared) {
2918
- if ((await sha256File(join(root, ...item.split("/")))) !== manifest.files[item]) {
2919
- fail(`OpenCode installed file checksum mismatch for ${item}`);
2920
- }
2921
- }
2922
- return manifest;
2923
- }
2924
-
2925
2594
  async function verifyManagedTreeForPurge(host, root) {
2926
- if (host === "opencode") return verifyOpenCodePurgeRoot(root);
2927
2595
  await requireOwnedRoot(root, host);
2928
2596
  const layout = HOST_LAYOUTS[host];
2929
2597
  const pluginRoot = join(root, layout.pluginRelative);
2930
2598
  const identity = await packageIdentityForPurge(pluginRoot, host, {
2931
- allowedExtra: (item) => FILE_DROP_HOSTS.includes(host) && item === MARKER_NAME,
2599
+ allowedExtra: (item) => false,
2932
2600
  });
2933
- if (FILE_DROP_HOSTS.includes(host)) return identity;
2934
2601
 
2935
2602
  const marketplacePath = join(root, layout.marketplaceRelative);
2936
2603
  await requireRegularFileEntry(marketplacePath, `${host} marketplace manifest`);
2937
2604
  const marketplace = await readJsonObject(marketplacePath);
2938
2605
  const expected = expectedMarketplace(host);
2939
- if (host === "claude") expected.metadata.version = identity.version;
2606
+
2940
2607
  if (!jsonDeepEqual(marketplace, expected)) {
2941
2608
  fail(`${host} managed marketplace does not match the exact owned plugin inventory`);
2942
2609
  }
2943
2610
  const allowed = new Set([
2944
2611
  MARKER_NAME,
2945
2612
  layout.marketplaceRelative.split(sep).join("/"),
2946
- ...identity.files.map((item) => join(layout.pluginRelative, ...item.split("/")).split(sep).join("/")),
2613
+ ...identity.files.map((item) =>
2614
+ join(layout.pluginRelative, ...item.split("/"))
2615
+ .split(sep)
2616
+ .join("/"),
2617
+ ),
2947
2618
  ]);
2948
2619
  for (const item of await walkFiles(root)) {
2949
2620
  if (!allowed.has(item)) fail(`${host} managed marketplace contains an unowned file: ${item}`);
@@ -2965,10 +2636,13 @@ async function removeOwnedManagedRoot(host, paths) {
2965
2636
  await rm(backup, { recursive: true });
2966
2637
  } catch (error) {
2967
2638
  if ((await entryExists(backup)) && !(await entryExists(paths.root))) {
2968
- const restored = await recoveryStep(`restore the ${host} managed root after purge cleanup failed`, async () => {
2969
- await verifyManagedTreeForPurge(host, backup);
2970
- await rename(backup, paths.root);
2971
- });
2639
+ const restored = await recoveryStep(
2640
+ `restore the ${host} managed root after purge cleanup failed`,
2641
+ async () => {
2642
+ await verifyManagedTreeForPurge(host, backup);
2643
+ await rename(backup, paths.root);
2644
+ },
2645
+ );
2972
2646
  if (!restored) {
2973
2647
  console.error(`error: recoverable ${host} files remain at: ${backup}`);
2974
2648
  console.error(`error: retry command: opensocrates remove --host ${host} --purge`);
@@ -2984,7 +2658,9 @@ async function cleanupTransientRootResidue(host, paths) {
2984
2658
  if (!(await entryExists(parent))) {
2985
2659
  return { component: "transaction-residue", status: "absent", path: parent };
2986
2660
  }
2987
- await requireSafePathBelow(paths.hostHome, parent, `${host} transient parent`, { allowRoot: true });
2661
+ await requireSafePathBelow(paths.hostHome, parent, `${host} transient parent`, {
2662
+ allowRoot: true,
2663
+ });
2988
2664
  const names = (await readdir(parent)).filter((name) =>
2989
2665
  /^\.opensocrates\.(?:staging|backup|removed)-[A-Za-z0-9-]+$/u.test(name),
2990
2666
  );
@@ -3014,97 +2690,6 @@ async function cleanupTransientRootResidue(host, paths) {
3014
2690
  };
3015
2691
  }
3016
2692
 
3017
- async function readOpenCodeBridgeSidecar(target) {
3018
- const info = await lstat(target);
3019
- if (!info.isFile() || info.isSymbolicLink()) {
3020
- fail(`OpenCode bridge ownership sidecar is unsafe: ${target}`);
3021
- }
3022
- const sidecar = await readJsonObject(target);
3023
- const expectedKeys = [...Object.keys(OPENCODE_MARKER), "version", "bridgeSha256"].sort();
3024
- const observedKeys = Object.keys(sidecar).sort();
3025
- const marker = Object.fromEntries(Object.keys(OPENCODE_MARKER).map((key) => [key, sidecar[key]]));
3026
- if (
3027
- expectedKeys.length !== observedKeys.length ||
3028
- expectedKeys.some((key, index) => key !== observedKeys[index]) ||
3029
- !markerMatches(marker, "opencode") ||
3030
- typeof sidecar.version !== "string" ||
3031
- sidecar.version.trim().length === 0 ||
3032
- !/^[a-f0-9]{64}$/u.test(sidecar.bridgeSha256)
3033
- ) {
3034
- fail(`OpenCode bridge ownership sidecar is invalid: ${target}`);
3035
- }
3036
- return sidecar;
3037
- }
3038
-
3039
- async function purgeOpenCodeBridge(paths) {
3040
- const bridgePresent = await entryExists(paths.bridge);
3041
- const markerPresent = await entryExists(paths.bridgeMarker);
3042
- if (!bridgePresent && !markerPresent) {
3043
- return { component: "opencode-bridge", status: "absent", path: paths.bridge };
3044
- }
3045
- await requireSafePathBelow(paths.hostHome, paths.bridge, "OpenCode bridge");
3046
- await requireSafePathBelow(paths.hostHome, paths.bridgeMarker, "OpenCode bridge sidecar");
3047
- if (bridgePresent && !markerPresent) {
3048
- fail("refusing to remove an OpenCode bridge without its ownership sidecar");
3049
- }
3050
- const sidecar = await readOpenCodeBridgeSidecar(paths.bridgeMarker);
3051
- if (bridgePresent) {
3052
- const bridgeInfo = await lstat(paths.bridge);
3053
- if (!bridgeInfo.isFile() || bridgeInfo.isSymbolicLink() || (await sha256File(paths.bridge)) !== sidecar.bridgeSha256) {
3054
- fail("refusing to remove an OpenCode bridge whose ownership checksum does not match");
3055
- }
3056
- await rm(paths.bridge);
3057
- }
3058
- await rm(paths.bridgeMarker);
3059
- return { component: "opencode-bridge", status: "removed", path: paths.bridge };
3060
- }
3061
-
3062
- async function purgeOpenCodeBridgeResidue(paths) {
3063
- if (!(await entryExists(paths.bridgeParent))) {
3064
- return { component: "opencode-bridge-residue", status: "absent", path: paths.bridgeParent };
3065
- }
3066
- await requireSafePathBelow(paths.hostHome, paths.bridgeParent, "OpenCode bridge parent");
3067
- const entries = await readdir(paths.bridgeParent, { withFileTypes: true });
3068
- const bridgeNames = entries
3069
- .filter((entry) => /^\.opensocrates\.js\.(?:staging|backup|removed)-[A-Za-z0-9-]+$/u.test(entry.name))
3070
- .map((entry) => entry.name);
3071
- const markerNames = entries
3072
- .filter((entry) =>
3073
- /^\.opensocrates-managed\.json\.(?:staging|backup|removed)-[A-Za-z0-9-]+$/u.test(entry.name),
3074
- )
3075
- .map((entry) => entry.name);
3076
- const bridges = new Map();
3077
- for (const name of bridgeNames) {
3078
- const target = join(paths.bridgeParent, name);
3079
- await requireSafePathBelow(paths.bridgeParent, target, "OpenCode bridge residue");
3080
- const info = await lstat(target);
3081
- if (!info.isFile() || info.isSymbolicLink()) fail(`unsafe OpenCode bridge residue: ${target}`);
3082
- bridges.set(target, await sha256File(target));
3083
- }
3084
- let removed = 0;
3085
- for (const name of markerNames) {
3086
- const marker = join(paths.bridgeParent, name);
3087
- await requireSafePathBelow(paths.bridgeParent, marker, "OpenCode bridge sidecar residue");
3088
- const sidecar = await readOpenCodeBridgeSidecar(marker);
3089
- const matches = [...bridges].filter(([, digest]) => digest === sidecar.bridgeSha256);
3090
- if (matches.length > 1) fail("ambiguous OpenCode bridge transaction residue");
3091
- if (matches.length === 1) {
3092
- const [[bridge]] = matches;
3093
- await rm(bridge);
3094
- bridges.delete(bridge);
3095
- removed += 1;
3096
- }
3097
- await rm(marker);
3098
- removed += 1;
3099
- }
3100
- if (bridges.size > 0) fail("refusing OpenCode bridge residue without an ownership sidecar");
3101
- return {
3102
- component: "opencode-bridge-residue",
3103
- status: removed > 0 ? "removed" : "absent",
3104
- path: paths.bridgeParent,
3105
- };
3106
- }
3107
-
3108
2693
  async function prepareVerifiedPackage(options, host = options.host) {
3109
2694
  if (!SUPPORTED_HOSTS.includes(host)) {
3110
2695
  fail(`cannot prepare a package for unsupported host ${JSON.stringify(host)}`);
@@ -3124,35 +2709,25 @@ async function prepareVerifiedPackage(options, host = options.host) {
3124
2709
  }
3125
2710
 
3126
2711
  async function prepareVerifiedPackages(options, hosts) {
3127
- const settled = await Promise.allSettled(hosts.map((host) => prepareVerifiedPackage(options, host)));
3128
- const prepared = settled.filter((result) => result.status === "fulfilled").map((result) => result.value);
3129
- const failure = settled.find((result) => result.status === "rejected");
3130
- if (failure) {
2712
+ const settled = await Promise.allSettled(
2713
+ hosts.map((host) => prepareVerifiedPackage(options, host)),
2714
+ );
2715
+ const prepared = settled
2716
+ .filter((result) => result.status === "fulfilled")
2717
+ .map((result) => result.value);
2718
+ if (settled.some((result) => result.status === "rejected")) {
3131
2719
  await Promise.all(prepared.map((item) => rm(item.scratch, { recursive: true, force: true })));
3132
- throw failure.reason;
2720
+ throw new PackagePreparationError(
2721
+ settled.map((result, index) => ({
2722
+ host: hosts[index],
2723
+ error: result.status === "rejected" ? result.reason : null,
2724
+ })),
2725
+ );
3133
2726
  }
3134
2727
  return prepared;
3135
2728
  }
3136
2729
 
3137
2730
  function expectedMarketplace(host) {
3138
- if (host === "claude") {
3139
- return {
3140
- name: MARKETPLACE_NAME,
3141
- owner: { name: "Parker Hwang" },
3142
- metadata: {
3143
- description: "OpenSocrates reasoning support for Claude Code and Cowork",
3144
- version: PRODUCT_VERSION,
3145
- },
3146
- plugins: [
3147
- {
3148
- name: PLUGIN_NAME,
3149
- source: `./plugins/${PLUGIN_NAME}`,
3150
- description: "Local reasoning-system selection for Claude Code and Cowork, plus one /opensocrates entry.",
3151
- category: "workflow",
3152
- },
3153
- ],
3154
- };
3155
- }
3156
2731
  return {
3157
2732
  name: MARKETPLACE_NAME,
3158
2733
  interface: { displayName: "OpenSocrates" },
@@ -3177,15 +2752,6 @@ async function buildStagingTree(parent, pluginSource, host) {
3177
2752
  const layout = HOST_LAYOUTS[host];
3178
2753
  const staging = await mkdtemp(join(parent, ".opensocrates.staging-"));
3179
2754
  try {
3180
- if (FILE_DROP_HOSTS.includes(host)) {
3181
- await cp(pluginSource, staging, { recursive: true, preserveTimestamps: true });
3182
- await verifyExtractedPackage(staging, host);
3183
- await writeFile(join(staging, MARKER_NAME), `${JSON.stringify(markerFor(host), null, 2)}\n`, {
3184
- encoding: "utf8",
3185
- mode: 0o600,
3186
- });
3187
- return staging;
3188
- }
3189
2755
  const marketplace = join(staging, layout.marketplaceRelative);
3190
2756
  const plugin = join(staging, layout.pluginRelative);
3191
2757
  await mkdir(dirname(marketplace), { recursive: true, mode: 0o700 });
@@ -3210,88 +2776,12 @@ async function buildStagingTree(parent, pluginSource, host) {
3210
2776
  }
3211
2777
  }
3212
2778
 
3213
- async function buildOpenCodeStaging(paths, pluginSource) {
3214
- await ensureOpenCodeDirectories(paths);
3215
- const root = await mkdtemp(join(paths.parent, ".opensocrates.staging-"));
3216
- const bridge = join(paths.bridgeParent, `.opensocrates.js.staging-${randomUUID()}`);
3217
- const bridgeMarker = join(paths.bridgeParent, `.opensocrates-managed.json.staging-${randomUUID()}`);
3218
- try {
3219
- await cp(join(pluginSource, "skills", "opensocrates"), root, {
3220
- recursive: true,
3221
- preserveTimestamps: true,
3222
- });
3223
- const metadata = join(root, ".opensocrates-package");
3224
- await mkdir(metadata, { mode: 0o700 });
3225
- for (const name of ["opencode-plugin.json", "release-manifest.json", "checksums.sha256"]) {
3226
- await cp(join(pluginSource, name), join(metadata, name), {
3227
- preserveTimestamps: true,
3228
- });
3229
- }
3230
- await writeFile(join(root, MARKER_NAME), `${JSON.stringify(markerFor("opencode"), null, 2)}\n`, {
3231
- encoding: "utf8",
3232
- mode: 0o600,
3233
- });
3234
- await cp(join(pluginSource, "plugins", "opensocrates.js"), bridge, {
3235
- preserveTimestamps: true,
3236
- });
3237
- const bridgeSha256 = await sha256File(bridge);
3238
- const files = {};
3239
- for (const item of (await walkFiles(root)).sort()) {
3240
- files[item] = await sha256File(join(root, ...item.split("/")));
3241
- }
3242
- await writeFile(
3243
- join(root, OPENCODE_INSTALL_MANIFEST),
3244
- `${JSON.stringify(
3245
- {
3246
- schema: "opensocrates.opencode-installation/1.0.0",
3247
- version: PRODUCT_VERSION,
3248
- bridgeSha256,
3249
- files,
3250
- },
3251
- null,
3252
- 2,
3253
- )}\n`,
3254
- { encoding: "utf8", mode: 0o600 },
3255
- );
3256
- await writeFile(bridgeMarker, `${JSON.stringify(openCodeSidecar(bridgeSha256), null, 2)}\n`, {
3257
- encoding: "utf8",
3258
- mode: 0o600,
3259
- });
3260
- return { root, bridge, bridgeMarker };
3261
- } catch (error) {
3262
- await rm(root, { recursive: true, force: true });
3263
- await rm(bridge, { force: true });
3264
- await rm(bridgeMarker, { force: true });
3265
- throw error;
3266
- }
3267
- }
3268
-
3269
2779
  function entryRoot(entry, host) {
3270
2780
  if (entry === null) {
3271
2781
  return null;
3272
2782
  }
3273
2783
  let value;
3274
- if (host === "claude") {
3275
- const fields = ["path", "installLocation"].filter((field) => entry[field] !== undefined && entry[field] !== null);
3276
- if (
3277
- fields.length === 0 ||
3278
- fields.some((field) => typeof entry[field] !== "string" || entry[field].trim().length === 0)
3279
- ) {
3280
- fail(`${host} marketplace ${MARKETPLACE_NAME} has no usable root`);
3281
- }
3282
- const roots = fields.map((field) => {
3283
- const candidate = resolve(entry[field]);
3284
- try {
3285
- return realpathSync(candidate);
3286
- } catch {
3287
- return candidate;
3288
- }
3289
- });
3290
- if (new Set(roots).size !== 1) {
3291
- fail(`${host} marketplace ${MARKETPLACE_NAME} reported conflicting roots`);
3292
- }
3293
- [value] = roots;
3294
- } else {
2784
+ {
3295
2785
  value = entry.root;
3296
2786
  }
3297
2787
  if (typeof value !== "string" || value.trim().length === 0) {
@@ -3310,16 +2800,6 @@ function entryRoot(entry, host) {
3310
2800
  }
3311
2801
 
3312
2802
  function removeRegistration(host, entry, state) {
3313
- if (FILE_DROP_HOSTS.includes(host) || host === "opencode") return;
3314
- if (host === "claude") {
3315
- if (["installed", "disabled"].includes(state.kind)) {
3316
- run(claudeBinary(), ["plugin", "uninstall", PLUGIN_ID, "--scope", "user"]);
3317
- }
3318
- if (entry !== null) {
3319
- run(claudeBinary(), ["plugin", "marketplace", "remove", MARKETPLACE_NAME, "--scope", "user"]);
3320
- }
3321
- return;
3322
- }
3323
2803
  if (state.kind === "installed") {
3324
2804
  runCodexJson(["plugin", "remove", PLUGIN_ID, "--json"]);
3325
2805
  }
@@ -3329,17 +2809,6 @@ function removeRegistration(host, entry, state) {
3329
2809
  }
3330
2810
 
3331
2811
  function addRegistration(host, root, installPlugin, { enabled = true } = {}) {
3332
- if (FILE_DROP_HOSTS.includes(host) || host === "opencode") return null;
3333
- if (host === "claude") {
3334
- run(claudeBinary(), ["plugin", "marketplace", "add", root, "--scope", "user"]);
3335
- if (installPlugin) {
3336
- run(claudeBinary(), ["plugin", "install", PLUGIN_ID, "--scope", "user"]);
3337
- if (!enabled) {
3338
- run(claudeBinary(), ["plugin", "disable", PLUGIN_ID, "--scope", "user"]);
3339
- }
3340
- }
3341
- return null;
3342
- }
3343
2812
  runCodexJson(["plugin", "marketplace", "add", root, "--json"]);
3344
2813
  if (installPlugin) {
3345
2814
  return runCodexJson(["plugin", "add", PLUGIN_ID, "--json"]);
@@ -3348,7 +2817,6 @@ function addRegistration(host, root, installPlugin, { enabled = true } = {}) {
3348
2817
  }
3349
2818
 
3350
2819
  function removeRegistrationBestEffort(host) {
3351
- if (FILE_DROP_HOSTS.includes(host) || host === "opencode") return;
3352
2820
  const entry = (() => {
3353
2821
  try {
3354
2822
  return marketplaceEntry(host);
@@ -3362,21 +2830,15 @@ function removeRegistrationBestEffort(host) {
3362
2830
  try {
3363
2831
  const state = pluginState(host);
3364
2832
  if (["installed", "disabled"].includes(state.kind)) {
3365
- const binary = host === "claude" ? claudeBinary() : codexBinary();
3366
- const args =
3367
- host === "claude"
3368
- ? ["plugin", "uninstall", PLUGIN_ID, "--scope", "user"]
3369
- : ["plugin", "remove", PLUGIN_ID, "--json"];
2833
+ const binary = codexBinary();
2834
+ const args = ["plugin", "remove", PLUGIN_ID, "--json"];
3370
2835
  run(binary, args, { allowFailure: true });
3371
2836
  }
3372
2837
  } catch {
3373
2838
  // Rollback continues with marketplace cleanup.
3374
2839
  }
3375
- const binary = host === "claude" ? claudeBinary() : codexBinary();
3376
- const args =
3377
- host === "claude"
3378
- ? ["plugin", "marketplace", "remove", MARKETPLACE_NAME, "--scope", "user"]
3379
- : ["plugin", "marketplace", "remove", MARKETPLACE_NAME, "--json"];
2840
+ const binary = codexBinary();
2841
+ const args = ["plugin", "marketplace", "remove", MARKETPLACE_NAME, "--json"];
3380
2842
  run(binary, args, { allowFailure: true });
3381
2843
  }
3382
2844
 
@@ -3392,38 +2854,12 @@ async function recoveryStep(label, action) {
3392
2854
  }
3393
2855
 
3394
2856
  async function preflightHost(host, action) {
3395
- if (
3396
- (!FILE_DROP_HOSTS.includes(host) && host !== "opencode") ||
3397
- ["install", "update"].includes(action)
3398
- ) {
2857
+ {
3399
2858
  requireHostCli(host, { authenticated: ["install", "update"].includes(action) });
3400
2859
  }
3401
- if (host === "claude") {
3402
- if (["install", "update"].includes(action)) {
3403
- requireNoLegacyClaudeInstallation();
3404
- } else {
3405
- warnLegacyClaudeInstallation();
3406
- }
3407
- }
2860
+
3408
2861
  const paths = managedPaths(host);
3409
- if (host === "opencode") {
3410
- const presence = await Promise.all(
3411
- [paths.root, paths.bridge, paths.bridgeMarker].map((target) => entryExists(target)),
3412
- );
3413
- if (presence.some(Boolean) && !presence.every(Boolean)) {
3414
- fail("OpenCode has a partial or unowned OpenSocrates installation; refusing to replace it");
3415
- }
3416
- if (presence.every(Boolean)) await verifyOpenCodeInstallation(paths);
3417
- const previousEntry = presence.every(Boolean) ? { name: MARKETPLACE_NAME, root: paths.root } : null;
3418
- const previousState = previousEntry === null ? { kind: "missing", version: null } : pluginState(host);
3419
- return {
3420
- host,
3421
- paths,
3422
- previousEntry,
3423
- previousState,
3424
- rootExists: presence[0],
3425
- };
3426
- }
2862
+
3427
2863
  const previousEntry = marketplaceEntry(host);
3428
2864
  if (previousEntry !== null && entryRoot(previousEntry, host) !== paths.root) {
3429
2865
  fail(
@@ -3458,7 +2894,9 @@ async function preflightSelectedHosts(options, action, desiredState) {
3458
2894
  const candidates = assetHosts.length > 0 ? assetHosts : SUPPORTED_HOSTS;
3459
2895
  const desiredHosts = new Set(desiredState.installedHosts);
3460
2896
  const rootPresence = Object.fromEntries(
3461
- await Promise.all(candidates.map(async (host) => [host, await entryExists(managedPaths(host).root)])),
2897
+ await Promise.all(
2898
+ candidates.map(async (host) => [host, await entryExists(managedPaths(host).root)]),
2899
+ ),
3462
2900
  );
3463
2901
  const settled = await Promise.allSettled(candidates.map((host) => preflightHost(host, action)));
3464
2902
  const successes = new Map();
@@ -3523,17 +2961,13 @@ async function stageInstallation(preflight, pluginSource) {
3523
2961
  await mkdir(paths.parent, { recursive: true, mode: 0o700 });
3524
2962
  const transient = transientParent(host, paths);
3525
2963
  await mkdir(transient, { recursive: true, mode: 0o700 });
3526
- const staging =
3527
- host === "opencode"
3528
- ? await buildOpenCodeStaging(paths, pluginSource)
3529
- : await buildStagingTree(transient, pluginSource, host);
2964
+ const staging = await buildStagingTree(transient, pluginSource, host);
3530
2965
  return {
3531
2966
  ...preflight,
3532
2967
  staging,
3533
2968
  backup: join(transient, `.opensocrates.backup-${randomUUID()}`),
3534
- bridgeBackup: host === "opencode" ? join(paths.bridgeParent, `.opensocrates.js.backup-${randomUUID()}`) : null,
3535
- bridgeMarkerBackup:
3536
- host === "opencode" ? join(paths.bridgeParent, `.opensocrates-managed.json.backup-${randomUUID()}`) : null,
2969
+ bridgeBackup: null,
2970
+ bridgeMarkerBackup: null,
3537
2971
  registrationRemoved: false,
3538
2972
  backupCreated: false,
3539
2973
  newRootActive: false,
@@ -3556,23 +2990,10 @@ async function activateInstallation(transaction) {
3556
2990
  await rename(paths.root, transaction.backup);
3557
2991
  transaction.backupCreated = true;
3558
2992
  }
3559
- if (host === "opencode" && (await entryExists(paths.bridge))) {
3560
- await rename(paths.bridge, transaction.bridgeBackup);
3561
- transaction.bridgeBackupCreated = true;
3562
- }
3563
- if (host === "opencode" && (await entryExists(paths.bridgeMarker))) {
3564
- await rename(paths.bridgeMarker, transaction.bridgeMarkerBackup);
3565
- transaction.bridgeMarkerBackupCreated = true;
3566
- }
3567
- await rename(host === "opencode" ? transaction.staging.root : transaction.staging, paths.root);
2993
+
2994
+ await rename(transaction.staging, paths.root);
3568
2995
  transaction.newRootActive = true;
3569
- if (host === "opencode") {
3570
- await rename(transaction.staging.bridge, paths.bridge);
3571
- transaction.newBridgeActive = true;
3572
- await rename(transaction.staging.bridgeMarker, paths.bridgeMarker);
3573
- transaction.newBridgeMarkerActive = true;
3574
- await verifyOpenCodeInstallation(paths);
3575
- }
2996
+
3576
2997
  const result = addRegistration(host, paths.root, true);
3577
2998
  const state = pluginState(host, { requireHostState: true });
3578
2999
  if (
@@ -3599,35 +3020,18 @@ async function rollbackInstallation(transaction) {
3599
3020
  await rm(paths.root, { recursive: true, force: true });
3600
3021
  });
3601
3022
  }
3602
- if (host === "opencode") {
3603
- if (transaction.newBridgeActive && (await exists(paths.bridge))) {
3604
- await recoveryStep("remove the failed OpenCode bridge", async () => {
3605
- await rm(paths.bridge, { force: true });
3606
- });
3607
- }
3608
- if (transaction.newBridgeMarkerActive && (await exists(paths.bridgeMarker))) {
3609
- await recoveryStep("remove the failed OpenCode bridge sidecar", async () => {
3610
- await rm(paths.bridgeMarker, { force: true });
3611
- });
3612
- }
3613
- if (transaction.bridgeBackupCreated && (await exists(transaction.bridgeBackup))) {
3614
- await recoveryStep("restore the previous OpenCode bridge", async () => {
3615
- await rename(transaction.bridgeBackup, paths.bridge);
3616
- });
3617
- }
3618
- if (transaction.bridgeMarkerBackupCreated && (await exists(transaction.bridgeMarkerBackup))) {
3619
- await recoveryStep("restore the previous OpenCode bridge sidecar", async () => {
3620
- await rename(transaction.bridgeMarkerBackup, paths.bridgeMarker);
3621
- });
3622
- }
3623
- }
3023
+
3624
3024
  if (transaction.backupCreated && (await exists(transaction.backup))) {
3625
3025
  restored = await recoveryStep(`restore the previous ${host} installation`, async () => {
3626
3026
  await requireOwnedRoot(transaction.backup, host);
3627
3027
  await rename(transaction.backup, paths.root);
3628
3028
  });
3629
3029
  }
3630
- if (transaction.registrationRemoved && previousEntry !== null && (restored || !transaction.backupCreated)) {
3030
+ if (
3031
+ transaction.registrationRemoved &&
3032
+ previousEntry !== null &&
3033
+ (restored || !transaction.backupCreated)
3034
+ ) {
3631
3035
  await recoveryStep(`re-register the previous ${host} installation`, async () => {
3632
3036
  addRegistration(host, paths.root, ["installed", "disabled"].includes(previousState.kind), {
3633
3037
  enabled: previousState.kind !== "disabled",
@@ -3635,20 +3039,20 @@ async function rollbackInstallation(transaction) {
3635
3039
  });
3636
3040
  }
3637
3041
  if (transaction.backupCreated && !restored) {
3638
- console.error(`error: the previous ${host} OpenSocrates installation could not be restored automatically.`);
3042
+ console.error(
3043
+ `error: the previous ${host} OpenSocrates installation could not be restored automatically.`,
3044
+ );
3639
3045
  console.error(`error: your previous files are preserved at: ${transaction.backup}`);
3640
3046
  console.error(`error: recovery command: /bin/rm -rf -- ${shellQuote(paths.root)}`);
3641
- console.error(`error: recovery command: /bin/mv -- ${shellQuote(transaction.backup)} ${shellQuote(paths.root)}`);
3047
+ console.error(
3048
+ `error: recovery command: /bin/mv -- ${shellQuote(transaction.backup)} ${shellQuote(paths.root)}`,
3049
+ );
3642
3050
  console.error(`error: recovery command: opensocrates install --host ${host}`);
3643
3051
  }
3644
3052
  }
3645
3053
 
3646
3054
  async function cleanupInstallationTransaction(transaction) {
3647
- if (transaction.host === "opencode") {
3648
- await rm(transaction.staging.root, { recursive: true, force: true });
3649
- await rm(transaction.staging.bridge, { force: true });
3650
- await rm(transaction.staging.bridgeMarker, { force: true });
3651
- } else if (await exists(transaction.staging)) {
3055
+ if (await exists(transaction.staging)) {
3652
3056
  await rm(transaction.staging, { recursive: true, force: true });
3653
3057
  }
3654
3058
  }
@@ -3659,14 +3063,6 @@ async function commitInstallation(transaction) {
3659
3063
  await rm(transaction.backup, { recursive: true });
3660
3064
  transaction.backupCreated = false;
3661
3065
  }
3662
- if (transaction.host === "opencode") {
3663
- for (const [created, backup] of [
3664
- [transaction.bridgeBackupCreated, transaction.bridgeBackup],
3665
- [transaction.bridgeMarkerBackupCreated, transaction.bridgeMarkerBackup],
3666
- ]) {
3667
- if (created && (await exists(backup))) await rm(backup);
3668
- }
3669
- }
3670
3066
  }
3671
3067
 
3672
3068
  function installedDesiredState(previous, hosts) {
@@ -3689,7 +3085,8 @@ async function runInstallOrUpdate(options, action) {
3689
3085
  for (const preflight of preflights) {
3690
3086
  const item = packages.get(preflight.host);
3691
3087
  console.log(
3692
- `${preflight.host}: verified OpenSocrates ${PRODUCT_VERSION} and ` + `${item.checkedFiles} package files`,
3088
+ `${preflight.host}: verified OpenSocrates ${PRODUCT_VERSION} and ` +
3089
+ `${item.checkedFiles} package files`,
3693
3090
  );
3694
3091
  transactions.push(await stageInstallation(preflight, item.pluginRoot));
3695
3092
  }
@@ -3708,9 +3105,12 @@ async function runInstallOrUpdate(options, action) {
3708
3105
  await Promise.all(prepared.map((item) => rm(item.scratch, { recursive: true, force: true })));
3709
3106
  }
3710
3107
  for (const transaction of transactions) {
3711
- const cleaned = await recoveryStep(`remove the committed ${transaction.host} backup`, async () => {
3712
- await commitInstallation(transaction);
3713
- });
3108
+ const cleaned = await recoveryStep(
3109
+ `remove the committed ${transaction.host} backup`,
3110
+ async () => {
3111
+ await commitInstallation(transaction);
3112
+ },
3113
+ );
3714
3114
  if (!cleaned) {
3715
3115
  console.warn(`warning: ${transaction.host} is active, but its previous backup needs cleanup`);
3716
3116
  }
@@ -3724,39 +3124,16 @@ async function runInstallOrUpdate(options, action) {
3724
3124
  "codex exec silently skips hooks that have not been trusted.",
3725
3125
  );
3726
3126
  }
3727
- if (hosts.includes("cursor")) {
3728
- console.log(
3729
- "Cursor: run Developer: Reload Window, then invoke /opensocrates from Agent chat. " +
3730
- "This experimental package adds no automatic OpenSocrates hook selector.",
3731
- );
3732
- }
3733
- if (hosts.includes("grok")) {
3734
- console.log(
3735
- "Grok Build: start a new task for automatic native-skill selection, or invoke /opensocrates explicitly.",
3736
- );
3737
- }
3738
- if (hosts.includes("opencode")) {
3739
- console.log(
3740
- "OpenCode: automatic same-turn selection uses the stable chat.message bridge; " +
3741
- "the native opensocrates skill remains available as an explicit fallback.",
3742
- );
3743
- }
3127
+
3744
3128
  console.log("Start new host tasks to load the updated OpenSocrates integration.");
3745
3129
  return hosts;
3746
3130
  }
3747
3131
 
3748
3132
  async function inspectHostStatus(host) {
3749
- if (!FILE_DROP_HOSTS.includes(host) && host !== "opencode") requireHostCli(host);
3750
- if (host === "claude") warnLegacyClaudeInstallation();
3133
+ requireHostCli(host);
3134
+
3751
3135
  const paths = managedPaths(host);
3752
- if (host === "opencode") {
3753
- const presence = await Promise.all([paths.root, paths.bridge, paths.bridgeMarker].map((target) => exists(target)));
3754
- if (!presence.some(Boolean)) return { host, kind: "missing", version: null, paths };
3755
- if (!presence.every(Boolean)) return { host, kind: "files-only", version: null, paths };
3756
- await verifyOpenCodeInstallation(paths);
3757
- const state = pluginState(host);
3758
- return { host, kind: state.kind, version: state.version, paths };
3759
- }
3136
+
3760
3137
  const entry = marketplaceEntry(host);
3761
3138
  if (entry === null) {
3762
3139
  if (await exists(paths.root)) {
@@ -3787,19 +3164,15 @@ async function showStatus(host) {
3787
3164
  const status = await inspectHostStatus(candidate);
3788
3165
  const expected = desired.installedHosts.includes(candidate);
3789
3166
  const hostDrift = expected
3790
- ? status.kind !== "installed" || desired.activeVersion === null || status.version !== desired.activeVersion
3167
+ ? status.kind !== "installed" ||
3168
+ desired.activeVersion === null ||
3169
+ status.version !== desired.activeVersion
3791
3170
  : status.kind !== "missing";
3792
3171
  drift ||= hostDrift;
3793
3172
  if (status.kind === "installed") {
3794
3173
  console.log(
3795
3174
  `${candidate}: installed ${status.version ?? "unknown"}` +
3796
- (["antigravity", "cursor"].includes(candidate)
3797
- ? " (experimental explicit-skill tier)"
3798
- : candidate === "grok"
3799
- ? " (native skill; automatic and explicit invocation)"
3800
- : candidate === "opencode"
3801
- ? " (stable same-turn bridge plus native skill fallback)"
3802
- : "") +
3175
+ "" +
3803
3176
  (expected
3804
3177
  ? hostDrift
3805
3178
  ? ` (drift from ${desired.activeVersion ?? "desired state"})`
@@ -3819,7 +3192,9 @@ async function showStatus(host) {
3819
3192
  } else if (status.kind === "files-only") {
3820
3193
  console.log(`${candidate}: managed files present but not registered`);
3821
3194
  } else {
3822
- console.log(`${candidate}: not installed` + (expected ? " (drift: desired host is missing)" : ""));
3195
+ console.log(
3196
+ `${candidate}: not installed` + (expected ? " (drift: desired host is missing)" : ""),
3197
+ );
3823
3198
  }
3824
3199
  } catch (error) {
3825
3200
  if (desired.installedHosts.includes(candidate)) drift = true;
@@ -3834,24 +3209,11 @@ async function showStatus(host) {
3834
3209
  }
3835
3210
  const status = await inspectHostStatus(host);
3836
3211
  if (status.kind === "installed") {
3837
- console.log(
3838
- `OpenSocrates ${status.version ?? "unknown"} is installed.` +
3839
- (host === "antigravity"
3840
- ? " Antigravity support is experimental and explicit-skill only."
3841
- : host === "cursor"
3842
- ? " Cursor support is experimental and explicit-skill first."
3843
- : host === "grok"
3844
- ? " Grok Build uses automatic native-skill selection and explicit /opensocrates invocation."
3845
- : host === "opencode"
3846
- ? " OpenCode uses the stable same-turn bridge and native skill fallback."
3847
- : ""),
3848
- );
3212
+ console.log(`OpenSocrates ${status.version ?? "unknown"} is installed.` + "");
3849
3213
  } else if (status.kind === "disabled") {
3850
3214
  console.log(
3851
3215
  `OpenSocrates ${status.version ?? "unknown"} is installed but disabled. ` +
3852
- (host === "grok"
3853
- ? "Remove the OpenSocrates disabled entry from Grok Build configuration, then run status again."
3854
- : "Run install or update to re-enable it."),
3216
+ "Run install or update to re-enable it.",
3855
3217
  );
3856
3218
  } else if (status.kind === "available") {
3857
3219
  console.log(`OpenSocrates ${status.version ?? "unknown"} is available but not installed.`);
@@ -3869,14 +3231,8 @@ function removalTransaction(preflight) {
3869
3231
  transientParent(preflight.host, preflight.paths),
3870
3232
  `.opensocrates.removed-${randomUUID()}`,
3871
3233
  ),
3872
- bridgeBackup:
3873
- preflight.host === "opencode"
3874
- ? join(preflight.paths.bridgeParent, `.opensocrates.js.removed-${randomUUID()}`)
3875
- : null,
3876
- bridgeMarkerBackup:
3877
- preflight.host === "opencode"
3878
- ? join(preflight.paths.bridgeParent, `.opensocrates-managed.json.removed-${randomUUID()}`)
3879
- : null,
3234
+ bridgeBackup: null,
3235
+ bridgeMarkerBackup: null,
3880
3236
  registrationRemoved: false,
3881
3237
  backupCreated: false,
3882
3238
  rootCommitted: false,
@@ -3897,20 +3253,16 @@ async function activateRemoval(transaction) {
3897
3253
  await rename(paths.root, transaction.backup);
3898
3254
  transaction.backupCreated = true;
3899
3255
  }
3900
- if (host === "opencode" && (await entryExists(paths.bridge))) {
3901
- await rename(paths.bridge, transaction.bridgeBackup);
3902
- transaction.bridgeBackupCreated = true;
3903
- }
3904
- if (host === "opencode" && (await entryExists(paths.bridgeMarker))) {
3905
- await rename(paths.bridgeMarker, transaction.bridgeMarkerBackup);
3906
- transaction.bridgeMarkerBackupCreated = true;
3907
- }
3908
3256
  }
3909
3257
 
3910
3258
  async function rollbackRemoval(transaction) {
3911
3259
  let complete = true;
3912
3260
  let restored = !transaction.rootExists;
3913
- if (transaction.rootExists && transaction.backupCreated && (await entryExists(transaction.backup))) {
3261
+ if (
3262
+ transaction.rootExists &&
3263
+ transaction.backupCreated &&
3264
+ (await entryExists(transaction.backup))
3265
+ ) {
3914
3266
  restored = await recoveryStep(`restore the removed ${transaction.host} files`, async () => {
3915
3267
  await verifyManagedTreeForPurge(transaction.host, transaction.backup);
3916
3268
  await rename(transaction.backup, transaction.paths.root);
@@ -3920,7 +3272,10 @@ async function rollbackRemoval(transaction) {
3920
3272
  try {
3921
3273
  await rename(transaction.paths.root, transaction.backup);
3922
3274
  } catch (requarantineError) {
3923
- const detail = requarantineError instanceof Error ? requarantineError.message : String(requarantineError);
3275
+ const detail =
3276
+ requarantineError instanceof Error
3277
+ ? requarantineError.message
3278
+ : String(requarantineError);
3924
3279
  transaction.backupCreated = await entryExists(transaction.backup);
3925
3280
  fail(
3926
3281
  `the restored ${transaction.host} tree failed verification and could not be returned to ` +
@@ -3943,52 +3298,22 @@ async function rollbackRemoval(transaction) {
3943
3298
  restored &&
3944
3299
  (await entryExists(transaction.paths.root))
3945
3300
  ) {
3946
- const registrationRestored = await recoveryStep(`restore the removed ${transaction.host} registration`, async () => {
3947
- addRegistration(
3948
- transaction.host,
3949
- transaction.paths.root,
3950
- ["installed", "disabled"].includes(transaction.previousState.kind),
3951
- { enabled: transaction.previousState.kind !== "disabled" },
3952
- );
3953
- });
3301
+ const registrationRestored = await recoveryStep(
3302
+ `restore the removed ${transaction.host} registration`,
3303
+ async () => {
3304
+ addRegistration(
3305
+ transaction.host,
3306
+ transaction.paths.root,
3307
+ ["installed", "disabled"].includes(transaction.previousState.kind),
3308
+ { enabled: transaction.previousState.kind !== "disabled" },
3309
+ );
3310
+ },
3311
+ );
3954
3312
  complete = registrationRestored && complete;
3955
3313
  } else if (transaction.registrationRemoved && transaction.previousEntry !== null) {
3956
3314
  complete = false;
3957
3315
  }
3958
- if (transaction.host === "opencode") {
3959
- let bridgeRestored = !transaction.rootExists;
3960
- if (
3961
- transaction.rootExists &&
3962
- transaction.bridgeBackupCreated &&
3963
- (await entryExists(transaction.bridgeBackup))
3964
- ) {
3965
- bridgeRestored = await recoveryStep("restore the removed OpenCode bridge", async () => {
3966
- await rename(transaction.bridgeBackup, transaction.paths.bridge);
3967
- transaction.bridgeBackupCreated = false;
3968
- });
3969
- } else if (transaction.rootExists && !transaction.bridgeCommitted && !transaction.bridgeBackupCreated) {
3970
- bridgeRestored = await entryExists(transaction.paths.bridge);
3971
- }
3972
- complete = bridgeRestored && complete;
3973
- let markerRestored = !transaction.rootExists;
3974
- if (
3975
- transaction.rootExists &&
3976
- transaction.bridgeMarkerBackupCreated &&
3977
- (await entryExists(transaction.bridgeMarkerBackup))
3978
- ) {
3979
- markerRestored = await recoveryStep("restore the removed OpenCode bridge sidecar", async () => {
3980
- await rename(transaction.bridgeMarkerBackup, transaction.paths.bridgeMarker);
3981
- transaction.bridgeMarkerBackupCreated = false;
3982
- });
3983
- } else if (
3984
- transaction.rootExists &&
3985
- !transaction.bridgeMarkerCommitted &&
3986
- !transaction.bridgeMarkerBackupCreated
3987
- ) {
3988
- markerRestored = await entryExists(transaction.paths.bridgeMarker);
3989
- }
3990
- complete = markerRestored && complete;
3991
- }
3316
+
3992
3317
  transaction.originalStateRestored = complete;
3993
3318
  return complete;
3994
3319
  }
@@ -3997,48 +3322,23 @@ async function commitRemoval(transaction, { beforeBackupDelete = null } = {}) {
3997
3322
  if (transaction.backupCreated && (await entryExists(transaction.backup))) {
3998
3323
  await verifyManagedTreeForPurge(transaction.host, transaction.backup);
3999
3324
  if (beforeBackupDelete !== null) {
4000
- await beforeBackupDelete(Object.freeze({
4001
- host: transaction.host,
4002
- backup: transaction.backup,
4003
- }));
3325
+ await beforeBackupDelete(
3326
+ Object.freeze({
3327
+ host: transaction.host,
3328
+ backup: transaction.backup,
3329
+ }),
3330
+ );
4004
3331
  }
4005
3332
  await rm(transaction.backup, { recursive: true });
4006
3333
  transaction.backupCreated = false;
4007
3334
  transaction.rootCommitted = true;
4008
3335
  }
4009
- if (transaction.host === "opencode") {
4010
- for (const [flag, backup, label] of [
4011
- ["bridgeBackupCreated", transaction.bridgeBackup, "bridge"],
4012
- ["bridgeMarkerBackupCreated", transaction.bridgeMarkerBackup, "bridge sidecar"],
4013
- ]) {
4014
- if (!transaction[flag] || !(await entryExists(backup))) continue;
4015
- const info = await lstat(backup);
4016
- if (!info.isFile() || info.isSymbolicLink()) {
4017
- fail(`refusing to remove an unsafe OpenCode ${label} backup: ${backup}`);
4018
- }
4019
- await rm(backup);
4020
- transaction[flag] = false;
4021
- if (flag === "bridgeBackupCreated") transaction.bridgeCommitted = true;
4022
- else transaction.bridgeMarkerCommitted = true;
4023
- }
4024
- }
4025
3336
  }
4026
3337
 
4027
3338
  async function validateRemovalCommit(transaction) {
4028
3339
  if (transaction.backupCreated && (await entryExists(transaction.backup))) {
4029
3340
  await verifyManagedTreeForPurge(transaction.host, transaction.backup);
4030
3341
  }
4031
- if (transaction.host !== "opencode") return;
4032
- for (const [created, backup, label] of [
4033
- [transaction.bridgeBackupCreated, transaction.bridgeBackup, "bridge"],
4034
- [transaction.bridgeMarkerBackupCreated, transaction.bridgeMarkerBackup, "bridge sidecar"],
4035
- ]) {
4036
- if (!created || !(await entryExists(backup))) continue;
4037
- const info = await lstat(backup);
4038
- if (!info.isFile() || info.isSymbolicLink()) {
4039
- fail(`refusing to remove an unsafe OpenCode ${label} backup: ${backup}`);
4040
- }
4041
- }
4042
3342
  }
4043
3343
 
4044
3344
  function xmlEscape(value) {
@@ -4057,7 +3357,9 @@ async function executablePath(name, environmentOverride) {
4057
3357
  await access(candidate, fsConstants.X_OK);
4058
3358
  return candidate;
4059
3359
  }
4060
- const which = run("/usr/bin/which", [name], { allowFailure: true });
3360
+ const which = run(process.platform === "win32" ? "where.exe" : "/usr/bin/which", [name], {
3361
+ allowFailure: true,
3362
+ });
4061
3363
  const candidate = which.status === 0 ? which.stdout.trim() : "";
4062
3364
  if (!candidate || !candidate.startsWith("/")) {
4063
3365
  fail(`could not find an executable ${name} for the automatic updater`);
@@ -4080,14 +3382,21 @@ function launchctlDomain() {
4080
3382
  function launchAgentDocument(npx, channel, environment) {
4081
3383
  const packageTag = channel === "next" ? "next" : "latest";
4082
3384
  const arguments_ = [npx, "--yes", `opensocrates@${packageTag}`, "auto-update", "run"];
4083
- const argumentsXml = arguments_.map((argument) => ` <string>${xmlEscape(argument)}</string>`).join("\n");
4084
- const environmentEntries = Object.entries(environment).sort(([left], [right]) => left.localeCompare(right));
3385
+ const argumentsXml = arguments_
3386
+ .map((argument) => ` <string>${xmlEscape(argument)}</string>`)
3387
+ .join("\n");
3388
+ const environmentEntries = Object.entries(environment).sort(([left], [right]) =>
3389
+ left.localeCompare(right),
3390
+ );
4085
3391
  const environmentXml =
4086
3392
  environmentEntries.length === 0
4087
3393
  ? ""
4088
3394
  : `\n <key>EnvironmentVariables</key>\n <dict>\n` +
4089
3395
  environmentEntries
4090
- .map(([key, value]) => ` <key>${xmlEscape(key)}</key>\n` + ` <string>${xmlEscape(value)}</string>`)
3396
+ .map(
3397
+ ([key, value]) =>
3398
+ ` <key>${xmlEscape(key)}</key>\n` + ` <string>${xmlEscape(value)}</string>`,
3399
+ )
4091
3400
  .join("\n") +
4092
3401
  "\n </dict>";
4093
3402
  return `<?xml version="1.0" encoding="UTF-8"?>
@@ -4120,6 +3429,7 @@ function launchAgentTarget() {
4120
3429
  }
4121
3430
 
4122
3431
  function launchAgentLoaded() {
3432
+ if (process.platform === "win32") return false;
4123
3433
  return (
4124
3434
  run(launchctlBinary(), ["print", launchAgentTarget()], {
4125
3435
  allowFailure: true,
@@ -4141,69 +3451,19 @@ function stopLoadedLaunchAgent() {
4141
3451
  async function updaterEnvironment(hosts, npx) {
4142
3452
  const node = await executablePath("node", "OPENSOCRATES_NODE_BIN");
4143
3453
  const environment = {
4144
- PATH: [...new Set([dirname(npx), dirname(node), "/usr/bin", "/bin", "/usr/sbin", "/sbin"])].join(":"),
3454
+ PATH: [
3455
+ ...new Set([dirname(npx), dirname(node), "/usr/bin", "/bin", "/usr/sbin", "/sbin"]),
3456
+ ].join(":"),
4145
3457
  };
4146
3458
  for (const host of hosts) {
4147
- if (host === "cursor") {
4148
- if (process.env.CURSOR_CONFIG_DIR) {
4149
- environment.CURSOR_CONFIG_DIR = resolve(process.env.CURSOR_CONFIG_DIR);
4150
- continue;
4151
- }
4152
- const app = cursorAppPaths().find((candidate) => existsSync(candidate));
4153
- if (app) continue;
4154
- const executable = await executablePath("cursor", "CURSOR_BIN");
4155
- environment.CURSOR_BIN = executable;
4156
- environment.PATH = [...new Set([dirname(executable), ...environment.PATH.split(":")])].join(":");
4157
- continue;
4158
- }
4159
- if (host === "antigravity") {
4160
- if (process.env.ANTIGRAVITY_CONFIG_DIR) {
4161
- environment.ANTIGRAVITY_CONFIG_DIR = resolve(process.env.ANTIGRAVITY_CONFIG_DIR);
4162
- continue;
4163
- }
4164
- const executable = await executablePath("agy", "AGY_BIN");
4165
- environment.AGY_BIN = executable;
4166
- environment.PATH = [...new Set([dirname(executable), ...environment.PATH.split(":")])].join(":");
4167
- continue;
4168
- }
4169
- if (host === "opencode") {
4170
- // OPENCODE_CONFIG_DIR redirects configuration only. Unlike the
4171
- // content-only hosts above, a scheduled OpenCode update re-runs the same
4172
- // requireHostCli version gate as an interactive install, so recording
4173
- // the config directory and stopping would leave the LaunchAgent without
4174
- // a resolvable opencode: enable would succeed in a shell that has it on
4175
- // PATH, then every scheduled run would fail the gate whenever opencode
4176
- // lives outside the launchd default PATH.
4177
- if (process.env.OPENCODE_CONFIG_DIR) {
4178
- environment.OPENCODE_CONFIG_DIR = resolve(process.env.OPENCODE_CONFIG_DIR);
4179
- }
4180
- const executable = await executablePath("opencode", "OPENCODE_BIN");
4181
- environment.OPENCODE_BIN = executable;
4182
- environment.PATH = [...new Set([dirname(executable), ...environment.PATH.split(":")])].join(":");
4183
- continue;
4184
- }
4185
- if (host === "grok") {
4186
- const executable = await executablePath("grok", "GROK_BIN");
4187
- environment.GROK_BIN = executable;
4188
- environment.PATH = [...new Set([dirname(executable), ...environment.PATH.split(":")])].join(
4189
- ":",
4190
- );
4191
- continue;
4192
- }
4193
- const key = host === "claude" ? "CLAUDE_BIN" : "CODEX_BIN";
3459
+ const key = "CODEX_BIN";
4194
3460
  const executable = await executablePath(host, key);
4195
3461
  environment[key] = executable;
4196
- environment.PATH = [...new Set([dirname(executable), ...environment.PATH.split(":")])].join(":");
4197
- }
4198
- for (const key of [
4199
- "ANTIGRAVITY_CONFIG_DIR",
4200
- "CLAUDE_CONFIG_DIR",
4201
- "CODEX_HOME",
4202
- "CURSOR_CONFIG_DIR",
4203
- "GROK_HOME",
4204
- "OPENCODE_CONFIG_DIR",
4205
- "OPENSOCRATES_STATE_DIR",
4206
- ]) {
3462
+ environment.PATH = [...new Set([dirname(executable), ...environment.PATH.split(":")])].join(
3463
+ ":",
3464
+ );
3465
+ }
3466
+ for (const key of ["CODEX_HOME", "OPENSOCRATES_STATE_DIR"]) {
4207
3467
  if (process.env[key]) environment[key] = resolve(process.env[key]);
4208
3468
  }
4209
3469
  return environment;
@@ -4211,7 +3471,10 @@ async function updaterEnvironment(hosts, npx) {
4211
3471
 
4212
3472
  async function installLaunchAgent(channel, hosts) {
4213
3473
  if (process.platform !== "darwin" || process.arch !== "arm64") {
4214
- fail(`automatic updates currently support darwin-arm64 only; detected ` + `${process.platform}-${process.arch}`);
3474
+ fail(
3475
+ `automatic updates currently support darwin-arm64 only; detected ` +
3476
+ `${process.platform}-${process.arch}`,
3477
+ );
4215
3478
  }
4216
3479
  const paths = statePaths();
4217
3480
  const npx = await executablePath("npx", "OPENSOCRATES_NPX_BIN");
@@ -4232,16 +3495,22 @@ async function installLaunchAgent(channel, hosts) {
4232
3495
  const wasLoaded = launchAgentLoaded();
4233
3496
  if (wasLoaded) stopLoadedLaunchAgent();
4234
3497
  await atomicWritePrivateFile(paths.launchAgent, document);
4235
- const launched = run(launchctlBinary(), ["bootstrap", launchctlDomain(), paths.launchAgent], { allowFailure: true });
3498
+ const launched = run(launchctlBinary(), ["bootstrap", launchctlDomain(), paths.launchAgent], {
3499
+ allowFailure: true,
3500
+ });
4236
3501
  if (launched.status !== 0) {
4237
3502
  if (previousDocument === null) {
4238
3503
  await rm(paths.launchAgent, { force: true });
4239
3504
  } else {
4240
3505
  await atomicWritePrivateFile(paths.launchAgent, previousDocument);
4241
3506
  if (wasLoaded) {
4242
- const restored = run(launchctlBinary(), ["bootstrap", launchctlDomain(), paths.launchAgent], {
4243
- allowFailure: true,
4244
- });
3507
+ const restored = run(
3508
+ launchctlBinary(),
3509
+ ["bootstrap", launchctlDomain(), paths.launchAgent],
3510
+ {
3511
+ allowFailure: true,
3512
+ },
3513
+ );
4245
3514
  if (restored.status !== 0) {
4246
3515
  console.error("warning: the previous OpenSocrates LaunchAgent could not be reloaded");
4247
3516
  }
@@ -4267,7 +3536,9 @@ async function requireOwnedLaunchAgent(target) {
4267
3536
  ];
4268
3537
  const arguments_ =
4269
3538
  argumentBlocks.length === 1
4270
- ? [...argumentBlocks[0][1].matchAll(/<string>([\s\S]*?)<\/string>/gu)].map((match) => match[1].trim())
3539
+ ? [...argumentBlocks[0][1].matchAll(/<string>([\s\S]*?)<\/string>/gu)].map((match) =>
3540
+ match[1].trim(),
3541
+ )
4271
3542
  : [];
4272
3543
  if (
4273
3544
  [...document.matchAll(labelPattern)].length !== 1 ||
@@ -4286,7 +3557,11 @@ async function disableLaunchAgent() {
4286
3557
  const paths = statePaths();
4287
3558
  const agentPresent = await entryExists(paths.launchAgent);
4288
3559
  if (agentPresent) {
4289
- await requireSafePathBelow(paths.launchAgentsDirectory, paths.launchAgent, "OpenSocrates LaunchAgent");
3560
+ await requireSafePathBelow(
3561
+ paths.launchAgentsDirectory,
3562
+ paths.launchAgent,
3563
+ "OpenSocrates LaunchAgent",
3564
+ );
4290
3565
  await requireOwnedLaunchAgent(paths.launchAgent);
4291
3566
  }
4292
3567
  if (process.env.OPENSOCRATES_SKIP_LAUNCHCTL !== "1") {
@@ -4308,7 +3583,9 @@ async function runStandardRemove(options, { report = true, beforeBackupDelete =
4308
3583
  const preflights = await preflightSelectedHosts(options, "remove", desired);
4309
3584
  const removedHosts = preflights.map((item) => item.host);
4310
3585
  const remainingHosts = desired.installedHosts.filter((host) => !removedHosts.includes(host));
4311
- const remainingAutoUpdateHosts = desired.autoUpdate.hosts.filter((host) => !removedHosts.includes(host));
3586
+ const remainingAutoUpdateHosts = desired.autoUpdate.hosts.filter(
3587
+ (host) => !removedHosts.includes(host),
3588
+ );
4312
3589
  const keepAutoUpdate = desired.autoUpdate.enabled && remainingAutoUpdateHosts.length > 0;
4313
3590
  const nextDesired = {
4314
3591
  ...desired,
@@ -4358,11 +3635,16 @@ async function runStandardRemove(options, { report = true, beforeBackupDelete =
4358
3635
  rollbackComplete = restored && rollbackComplete;
4359
3636
  }
4360
3637
  const unrestoredHosts = new Set(
4361
- transactions.filter((transaction) => transaction.originalStateRestored !== true).map((item) => item.host),
3638
+ transactions
3639
+ .filter((transaction) => transaction.originalStateRestored !== true)
3640
+ .map((item) => item.host),
4362
3641
  );
4363
3642
  const recoveredHosts = desired.installedHosts.filter((host) => !unrestoredHosts.has(host));
4364
- const recoveredAutoUpdateHosts = desired.autoUpdate.hosts.filter((host) => recoveredHosts.includes(host));
4365
- const keepRecoveredAutoUpdate = desired.autoUpdate.enabled && recoveredAutoUpdateHosts.length > 0;
3643
+ const recoveredAutoUpdateHosts = desired.autoUpdate.hosts.filter((host) =>
3644
+ recoveredHosts.includes(host),
3645
+ );
3646
+ const keepRecoveredAutoUpdate =
3647
+ desired.autoUpdate.enabled && recoveredAutoUpdateHosts.length > 0;
4366
3648
  let recoveredDesired = {
4367
3649
  ...desired,
4368
3650
  installedHosts: recoveredHosts,
@@ -4373,13 +3655,16 @@ async function runStandardRemove(options, { report = true, beforeBackupDelete =
4373
3655
  };
4374
3656
  let schedulerRestored = true;
4375
3657
  if (schedulerTouched) {
4376
- schedulerRestored = await recoveryStep("reconcile the automatic updater after removal failure", async () => {
4377
- if (recoveredDesired.autoUpdate.enabled) {
4378
- await installLaunchAgent(recoveredDesired.channel, recoveredDesired.autoUpdate.hosts);
4379
- } else {
4380
- await disableLaunchAgent();
4381
- }
4382
- });
3658
+ schedulerRestored = await recoveryStep(
3659
+ "reconcile the automatic updater after removal failure",
3660
+ async () => {
3661
+ if (recoveredDesired.autoUpdate.enabled) {
3662
+ await installLaunchAgent(recoveredDesired.channel, recoveredDesired.autoUpdate.hosts);
3663
+ } else {
3664
+ await disableLaunchAgent();
3665
+ }
3666
+ },
3667
+ );
4383
3668
  if (!schedulerRestored && recoveredDesired.autoUpdate.enabled) {
4384
3669
  recoveredDesired = {
4385
3670
  ...recoveredDesired,
@@ -4393,16 +3678,21 @@ async function runStandardRemove(options, { report = true, beforeBackupDelete =
4393
3678
  rollbackComplete = schedulerRestored && rollbackComplete;
4394
3679
  }
4395
3680
  if (desiredWritten || !rollbackComplete || !schedulerRestored) {
4396
- const stateRestored = await recoveryStep("record desired state after removal failure", async () => {
4397
- await writeDesiredState(recoveredDesired);
4398
- });
3681
+ const stateRestored = await recoveryStep(
3682
+ "record desired state after removal failure",
3683
+ async () => {
3684
+ await writeDesiredState(recoveredDesired);
3685
+ },
3686
+ );
4399
3687
  rollbackComplete = stateRestored && rollbackComplete;
4400
3688
  }
4401
3689
  if (!rollbackComplete) {
4402
3690
  console.error("error: removal cleanup is incomplete; no success was recorded.");
4403
3691
  for (const transaction of transactions) {
4404
3692
  if (!transaction.backupCreated || !(await entryExists(transaction.backup))) continue;
4405
- console.error(`error: preserved ${transaction.host} removal residue: ${transaction.backup}`);
3693
+ console.error(
3694
+ `error: preserved ${transaction.host} removal residue: ${transaction.backup}`,
3695
+ );
4406
3696
  console.error(
4407
3697
  `error: safe cleanup retry: opensocrates remove --host ${transaction.host} --purge`,
4408
3698
  );
@@ -4416,22 +3706,13 @@ async function runStandardRemove(options, { report = true, beforeBackupDelete =
4416
3706
  }
4417
3707
  if (!report) return { removedHosts, desired: nextDesired };
4418
3708
  for (const host of removedHosts) {
4419
- const label =
4420
- host === "antigravity"
4421
- ? "Antigravity"
4422
- : host === "claude"
4423
- ? "Claude"
4424
- : host === "cursor"
4425
- ? "Cursor"
4426
- : host === "grok"
4427
- ? "Grok Build"
4428
- : host === "opencode"
4429
- ? "OpenCode"
4430
- : "Codex";
3709
+ const label = "Codex";
4431
3710
  console.log(`OpenSocrates was removed from ${label}.`);
4432
3711
  }
4433
3712
  if (removedHosts.length === 0) console.log("OpenSocrates is not installed on any managed host.");
4434
- console.log("Removal scope: registrations and installer-managed roots only; this is not a complete uninstall.");
3713
+ console.log(
3714
+ "Removal scope: registrations and installer-managed roots only; this is not a complete uninstall.",
3715
+ );
4435
3716
  const residue = [];
4436
3717
  for (const host of removedHosts) {
4437
3718
  const paths = purgePathsFor(host);
@@ -4446,14 +3727,13 @@ async function runStandardRemove(options, { report = true, beforeBackupDelete =
4446
3727
  if (removedHosts.includes("codex")) {
4447
3728
  console.log("Codex OpenSocrates hook trust is preserved by this removal contract.");
4448
3729
  }
4449
- console.log(`Next: close active hosts, then run opensocrates remove --host ${options.host} --purge.`);
3730
+ console.log(
3731
+ `Next: close active hosts, then run opensocrates remove --host ${options.host} --purge.`,
3732
+ );
4450
3733
  return { removedHosts, desired: nextDesired };
4451
3734
  }
4452
3735
 
4453
3736
  async function purgeRegistration(host, paths) {
4454
- if (FILE_DROP_HOSTS.includes(host) || host === "opencode") {
4455
- return { status: "not-applicable", detail: "file-owned-host" };
4456
- }
4457
3737
  try {
4458
3738
  requireHostCli(host);
4459
3739
  } catch (error) {
@@ -4462,7 +3742,7 @@ async function purgeRegistration(host, paths) {
4462
3742
  detail: error instanceof Error ? error.message : String(error),
4463
3743
  };
4464
3744
  }
4465
- if (host === "claude") warnLegacyClaudeInstallation();
3745
+
4466
3746
  const entry = marketplaceEntry(host);
4467
3747
  if (entry !== null && entryRoot(entry, host) !== paths.root) {
4468
3748
  fail(`${host} OpenSocrates registration points to an unmanaged location`);
@@ -4497,7 +3777,9 @@ function purgeComponentBlocksCompletion(component) {
4497
3777
  }
4498
3778
 
4499
3779
  function finalizePurgeHostResult(hostResult) {
4500
- const registrationComplete = new Set(["removed", "absent", "not-applicable"]).has(hostResult.registration);
3780
+ const registrationComplete = new Set(["removed", "absent", "not-applicable"]).has(
3781
+ hostResult.registration,
3782
+ );
4501
3783
  hostResult.status =
4502
3784
  registrationComplete &&
4503
3785
  !hostResult.components.some(purgeComponentBlocksCompletion) &&
@@ -4510,7 +3792,9 @@ function finalizePurgeHostResult(hostResult) {
4510
3792
  async function resetPurgeTrustExtension(hostResult, options) {
4511
3793
  if (hostResult.host !== "codex" || !options.resetTrust) return true;
4512
3794
  try {
4513
- const reset = await resetCodexOpenSocratesHookTrust({ hooks: options.trustResetHooks ?? {} });
3795
+ const reset = await resetCodexOpenSocratesHookTrust({
3796
+ hooks: options.trustResetHooks ?? {},
3797
+ });
4514
3798
  hostResult.extension = {
4515
3799
  component: "host-security-trust",
4516
3800
  status: reset.status,
@@ -4539,9 +3823,7 @@ function preservePurgeComponentsAfterTrustFailure(hostResult, paths) {
4539
3823
  ["managed-root", paths.root],
4540
3824
  ["transaction-residue", null],
4541
3825
  ];
4542
- if (hostResult.host === "opencode") {
4543
- preserved.push(["opencode-bridge", paths.bridge], ["opencode-bridge-residue", null]);
4544
- }
3826
+
4545
3827
  if (paths.cacheRoot !== null) preserved.push(["plugin-cache", paths.cacheRoot]);
4546
3828
  preserved.push(...paths.pluginData.map((path) => ["plugin-data", path]));
4547
3829
  for (const [component, path] of preserved) {
@@ -4576,30 +3858,25 @@ async function purgeOneHost(hostResult, options) {
4576
3858
  await capturePurgeComponent(hostResult, "managed-root", () =>
4577
3859
  removeOwnedManagedRoot(hostResult.host, paths),
4578
3860
  );
4579
- if (hostResult.host === "opencode") {
4580
- await capturePurgeComponent(hostResult, "opencode-bridge", () => purgeOpenCodeBridge(paths));
4581
- }
3861
+
4582
3862
  await capturePurgeComponent(hostResult, "transaction-residue", () =>
4583
3863
  cleanupTransientRootResidue(hostResult.host, paths),
4584
3864
  );
4585
- if (hostResult.host === "opencode") {
4586
- await capturePurgeComponent(hostResult, "opencode-bridge-residue", () =>
4587
- purgeOpenCodeBridgeResidue(paths),
4588
- );
4589
- }
3865
+
4590
3866
  if (paths.cacheRoot !== null) {
4591
- await capturePurgeComponent(hostResult, "plugin-cache", () => purgeHostCache(hostResult.host, paths));
4592
- }
4593
- if (paths.pluginData.length > 0) {
4594
- await capturePurgeComponent(hostResult, "plugin-data", () => purgeClaudePluginData(paths));
3867
+ await capturePurgeComponent(hostResult, "plugin-cache", () =>
3868
+ purgeHostCache(hostResult.host, paths),
3869
+ );
4595
3870
  }
4596
3871
  finalizePurgeHostResult(hostResult);
4597
3872
  }
4598
3873
 
4599
3874
  function hostDeactivated(hostResult) {
4600
- const registrationComplete = new Set(["removed", "absent", "not-applicable"]).has(hostResult.registration);
4601
- const activationComponents = hostResult.components.filter((item) =>
4602
- new Set(["managed-root", "opencode-bridge"]).has(item.component),
3875
+ const registrationComplete = new Set(["removed", "absent", "not-applicable"]).has(
3876
+ hostResult.registration,
3877
+ );
3878
+ const activationComponents = hostResult.components.filter(
3879
+ (item) => item.component === "managed-root",
4603
3880
  );
4604
3881
  return registrationComplete && !activationComponents.some(purgeComponentBlocksCompletion);
4605
3882
  }
@@ -4708,7 +3985,11 @@ async function preparePurgeStateFinalization() {
4708
3985
  const renamed = [];
4709
3986
  try {
4710
3987
  for (const item of tombstones) {
4711
- await requireSafePathBelow(paths.directory, item.tombstone, "OpenSocrates prior purge tombstone");
3988
+ await requireSafePathBelow(
3989
+ paths.directory,
3990
+ item.tombstone,
3991
+ "OpenSocrates prior purge tombstone",
3992
+ );
4712
3993
  const info = await lstat(item.tombstone);
4713
3994
  if (!info.isFile() || info.isSymbolicLink()) {
4714
3995
  fail(`refusing to remove an unsafe prior OpenSocrates purge tombstone: ${item.tombstone}`);
@@ -4721,7 +4002,10 @@ async function preparePurgeStateFinalization() {
4721
4002
  if (!info.isFile() || info.isSymbolicLink()) {
4722
4003
  fail(`refusing to remove an unsafe OpenSocrates state file: ${target}`);
4723
4004
  }
4724
- const tombstone = join(paths.directory, `.purge-finalize-${randomUUID()}-${basename(target)}`);
4005
+ const tombstone = join(
4006
+ paths.directory,
4007
+ `.purge-finalize-${randomUUID()}-${basename(target)}`,
4008
+ );
4725
4009
  await rename(target, tombstone);
4726
4010
  const item = { original: target, tombstone };
4727
4011
  tombstones.push(item);
@@ -4865,7 +4149,9 @@ function reportPurgeResult(result) {
4865
4149
  console.log("User task, project, chat, plan, and history data was preserved.");
4866
4150
  } else {
4867
4151
  console.error("OpenSocrates purge is incomplete; no complete-uninstall success is claimed.");
4868
- console.error("Resolve the reported item, close active hosts if needed, and rerun the same purge command.");
4152
+ console.error(
4153
+ "Resolve the reported item, close active hosts if needed, and rerun the same purge command.",
4154
+ );
4869
4155
  }
4870
4156
  }
4871
4157
 
@@ -4917,14 +4203,19 @@ async function enableAutoUpdate(options) {
4917
4203
  const observedVersions = new Set();
4918
4204
  for (const host of installedHosts) {
4919
4205
  const preflight = preflights.get(host) ?? (await preflightHost(host, "update"));
4920
- if (preflight.previousState.kind !== "installed" || typeof preflight.previousState.version !== "string") {
4206
+ if (
4207
+ preflight.previousState.kind !== "installed" ||
4208
+ typeof preflight.previousState.version !== "string"
4209
+ ) {
4921
4210
  fail(`cannot determine the installed OpenSocrates version on ${host}`);
4922
4211
  }
4923
4212
  preflights.set(host, preflight);
4924
4213
  observedVersions.add(preflight.previousState.version);
4925
4214
  }
4926
4215
  if (observedVersions.size !== 1) {
4927
- fail("installed hosts do not share one known version; run update --host all before enabling automatic updates");
4216
+ fail(
4217
+ "installed hosts do not share one known version; run update --host all before enabling automatic updates",
4218
+ );
4928
4219
  }
4929
4220
  [activeVersion] = observedVersions;
4930
4221
  }
@@ -4990,7 +4281,9 @@ async function runScheduledUpdate(options) {
4990
4281
  }
4991
4282
  const scheduledAt = Date.parse(desired.autoUpdate.nextCheckAt ?? "");
4992
4283
  if (!options.force && Number.isFinite(scheduledAt) && scheduledAt > Date.now()) {
4993
- console.log(`The next automatic update check is scheduled for ${desired.autoUpdate.nextCheckAt}.`);
4284
+ console.log(
4285
+ `The next automatic update check is scheduled for ${desired.autoUpdate.nextCheckAt}.`,
4286
+ );
4994
4287
  return;
4995
4288
  }
4996
4289
  const checkedAt = nowIso();
@@ -5020,7 +4313,9 @@ async function runScheduledUpdate(options) {
5020
4313
  result: "blocked",
5021
4314
  errorCategory: "major-policy",
5022
4315
  });
5023
- console.log(`OpenSocrates ${PRODUCT_VERSION} is available but blocked by the major-version policy.`);
4316
+ console.log(
4317
+ `OpenSocrates ${PRODUCT_VERSION} is available but blocked by the major-version policy.`,
4318
+ );
5024
4319
  return;
5025
4320
  }
5026
4321
  try {
@@ -5028,7 +4323,10 @@ async function runScheduledUpdate(options) {
5028
4323
  for (const host of hosts) ready.push(await preflightHost(host, "update"));
5029
4324
  const alreadyCurrent =
5030
4325
  desired.activeVersion === PRODUCT_VERSION &&
5031
- ready.every((item) => item.previousState.kind === "installed" && item.previousState.version === PRODUCT_VERSION);
4326
+ ready.every(
4327
+ (item) =>
4328
+ item.previousState.kind === "installed" && item.previousState.version === PRODUCT_VERSION,
4329
+ );
5032
4330
  if (!alreadyCurrent) {
5033
4331
  if (hosts.length > 1) {
5034
4332
  await runInstallOrUpdate({ ...options, host: ALL_HOST }, "update");
@@ -5080,7 +4378,14 @@ async function runScheduledUpdate(options) {
5080
4378
  await writeAutoUpdateReceipt({
5081
4379
  version: PRODUCT_VERSION,
5082
4380
  checkedAt,
5083
- hosts: hosts.map((host) => ({ host, result: "failed" })),
4381
+ hosts:
4382
+ error instanceof PackagePreparationError
4383
+ ? error.hostOutcomes
4384
+ : hosts.map((host) => ({
4385
+ host,
4386
+ result: "failed",
4387
+ errorCategory: errorCategory(error),
4388
+ })),
5084
4389
  result: "failed",
5085
4390
  errorCategory: errorCategory(error),
5086
4391
  });
@@ -5090,9 +4395,14 @@ async function runScheduledUpdate(options) {
5090
4395
  }
5091
4396
 
5092
4397
  function requireSupportedPlatform() {
5093
- if (process.platform !== "darwin" || process.arch !== "arm64") {
4398
+ if (
4399
+ !(
4400
+ (process.platform === "darwin" && process.arch === "arm64") ||
4401
+ (process.platform === "win32" && process.arch === "x64")
4402
+ )
4403
+ ) {
5094
4404
  fail(
5095
- `OpenSocrates ${PRODUCT_VERSION} prebuilt installation supports darwin-arm64 only; ` +
4405
+ `OpenSocrates ${PRODUCT_VERSION} prebuilt installation supports darwin-arm64 and windows-x64; ` +
5096
4406
  `detected ${process.platform}-${process.arch}`,
5097
4407
  );
5098
4408
  }
@@ -5101,26 +4411,18 @@ function requireSupportedPlatform() {
5101
4411
  async function verifyPackages(options) {
5102
4412
  const assetHosts = qualifiedAssetHosts(options);
5103
4413
  const hosts =
5104
- options.host === ALL_HOST ? (assetHosts.length > 0 ? assetHosts : SUPPORTED_HOSTS) : [options.host];
4414
+ options.host === ALL_HOST
4415
+ ? assetHosts.length > 0
4416
+ ? assetHosts
4417
+ : SUPPORTED_HOSTS
4418
+ : [options.host];
5105
4419
  const prepared = await prepareVerifiedPackages(options, hosts);
5106
4420
  try {
5107
4421
  for (const item of prepared) {
5108
4422
  console.log(
5109
- `${item.host}: verified OpenSocrates ${PRODUCT_VERSION} release and ` + `${item.checkedFiles} package files.`,
4423
+ `${item.host}: verified OpenSocrates ${PRODUCT_VERSION} release and ` +
4424
+ `${item.checkedFiles} package files.`,
5110
4425
  );
5111
- if (item.host === "opencode") {
5112
- const paths = managedPaths("opencode");
5113
- const presence = await Promise.all(
5114
- [paths.root, paths.bridge, paths.bridgeMarker].map((target) => exists(target)),
5115
- );
5116
- if (presence.some(Boolean)) {
5117
- if (!presence.every(Boolean)) {
5118
- fail("OpenCode installed state is partial and failed verification");
5119
- }
5120
- await verifyOpenCodeInstallation(paths);
5121
- console.log("opencode: verified installed bridge, skill inventory, and ownership.");
5122
- }
5123
- }
5124
4426
  }
5125
4427
  } finally {
5126
4428
  await Promise.all(prepared.map((item) => rm(item.scratch, { recursive: true, force: true })));
@@ -5129,6 +4431,28 @@ async function verifyPackages(options) {
5129
4431
 
5130
4432
  export async function main(argv = process.argv.slice(2), internalDependencies = {}) {
5131
4433
  const options = parseCli(argv);
4434
+ if (MACOS_PROFILE_HOSTS.includes(options.host) && options.action !== "help") {
4435
+ const { runManagedHost } = await import("./managed-hosts.mjs");
4436
+ const operation = () => runManagedHost(options, {
4437
+ version: PRODUCT_VERSION,
4438
+ platform: process.platform,
4439
+ claudeBinary: process.env.CLAUDE_BIN || "claude",
4440
+ preparePackage: prepareProfilePackage,
4441
+ });
4442
+ const result = ["status", "diagnose", "verify", "export"].includes(options.action)
4443
+ ? await operation() : await withOperationLock(operation);
4444
+ console.log(JSON.stringify(result, null, 2));
4445
+ return 0;
4446
+ }
4447
+ if (process.platform === "win32" && options.action === "auto-update") {
4448
+ const message =
4449
+ `Automatic updates: unavailable on Windows in ${PRODUCT_VERSION}. Use opensocrates update --host all manually.`;
4450
+ if (options.autoUpdateAction === "status" || options.autoUpdateAction === "disable") {
4451
+ console.log(message);
4452
+ return 0;
4453
+ }
4454
+ fail(message);
4455
+ }
5132
4456
  if (options.action === "help") {
5133
4457
  showHelp();
5134
4458
  return 0;