opensocrates 1.3.1 → 1.4.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.
@@ -34,27 +34,21 @@ import { Readable } from "node:stream";
34
34
  import { pipeline } from "node:stream/promises";
35
35
  import { fileURLToPath } from "node:url";
36
36
 
37
- export const PRODUCT_VERSION = "1.3.1";
37
+ export const PRODUCT_VERSION = "1.4.0";
38
38
  export const REPOSITORY = "ParkerHwang/OpenSocrates";
39
39
  export const MARKETPLACE_NAME = "opensocrates";
40
40
  export const PLUGIN_NAME = "opensocrates";
41
41
  export const PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
42
42
  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
- ]);
43
+ export const SUPPORTED_HOSTS = Object.freeze(["codex"]);
51
44
  export const ALL_HOST = "all";
52
45
  export const HOST_CHOICES = Object.freeze([ALL_HOST, ...SUPPORTED_HOSTS]);
53
46
  export function assetNameFor(host = DEFAULT_HOST) {
54
47
  if (!SUPPORTED_HOSTS.includes(host)) {
55
48
  fail(`unsupported host ${JSON.stringify(host)}`);
56
49
  }
57
- return `opensocrates-${PRODUCT_VERSION}-${host}-plugin.zip`;
50
+ const suffix = process.platform === "win32" ? "-windows-x64" : "";
51
+ return `opensocrates-${PRODUCT_VERSION}-${host}-plugin${suffix}.zip`;
58
52
  }
59
53
  export const ASSET_NAME = assetNameFor(DEFAULT_HOST);
60
54
  export const CHECKSUM_NAME = `${ASSET_NAME}.sha256`;
@@ -65,87 +59,18 @@ const CODEX_MARKER = Object.freeze({
65
59
  marketplaceName: MARKETPLACE_NAME,
66
60
  pluginName: PLUGIN_NAME,
67
61
  });
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
62
  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
63
  codex: {
121
64
  marketplaceRelative: join(".agents", "plugins", "marketplace.json"),
122
65
  pluginRelative: join("build", "generated", "plugins", "codex"),
123
66
  manifestRelative: join(".codex-plugin", "plugin.json"),
124
67
  requiresRuntime: true,
125
68
  },
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
69
  });
145
70
  const MAX_DOWNLOAD_BYTES = 2 * 1024 * 1024 * 1024;
146
71
  const MAX_ARCHIVE_ENTRIES = 10_000;
147
72
  const DESIRED_STATE_SCHEMA = "opensocrates.desired-state/1.0.0";
148
- const RECEIPT_SCHEMA = "opensocrates.auto-update-receipt/1.0.0";
73
+ const RECEIPT_SCHEMA = "opensocrates.auto-update-receipt/1.1.0";
149
74
  const AUTO_UPDATE_LABEL = "com.opensocrates.auto-update";
150
75
  const AUTO_UPDATE_MIN_INTERVAL_HOURS = 1;
151
76
  const AUTO_UPDATE_MAX_INTERVAL_HOURS = 24 * 7;
@@ -174,17 +99,12 @@ const MAX_CODEX_APP_SERVER_NOTIFICATIONS = 128;
174
99
  // Keep host security state separate from installer-owned payload cleanup so an
175
100
  // explicit trust reset can fail without weakening purge path-ownership checks.
176
101
  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
- };
102
+ if (host !== "codex") fail("only Codex is supported");
103
+ return {
104
+ component: "host-security-trust",
105
+ status: resetTrust ? "pending" : "preserved",
106
+ nextAction: resetTrust ? null : "rerun-purge-with-reset-trust",
107
+ };
188
108
  }
189
109
 
190
110
  export function createPurgeResult(hosts, { resetTrust = false } = {}) {
@@ -259,7 +179,11 @@ function decodeTomlLiteralString(source, start) {
259
179
  const end = source.indexOf("'", start + 1);
260
180
  if (end < 0) return null;
261
181
  const value = source.slice(start + 1, end);
262
- if ([...value].some((character) => character.codePointAt(0) < 0x20 || character.codePointAt(0) === 0x7f)) {
182
+ if (
183
+ [...value].some(
184
+ (character) => character.codePointAt(0) < 0x20 || character.codePointAt(0) === 0x7f,
185
+ )
186
+ ) {
263
187
  return null;
264
188
  }
265
189
  return { value, next: end + 1, kind: "literal", raw: source.slice(start, end + 1) };
@@ -374,7 +298,12 @@ function tomlLines(source) {
374
298
  while (start < source.length) {
375
299
  const newline = source.indexOf("\n", start);
376
300
  const end = newline < 0 ? source.length : newline + 1;
377
- const contentEnd = newline < 0 ? source.length : newline > start && source[newline - 1] === "\r" ? newline - 1 : newline;
301
+ const contentEnd =
302
+ newline < 0
303
+ ? source.length
304
+ : newline > start && source[newline - 1] === "\r"
305
+ ? newline - 1
306
+ : newline;
378
307
  lines.push({ start, end, contentEnd, body: source.slice(start, contentEnd) });
379
308
  start = end;
380
309
  }
@@ -386,7 +315,10 @@ function parseTomlTableHeader(code) {
386
315
  const trimmedEnd = code.trimEnd().length;
387
316
  const candidate = code.slice(leading, trimmedEnd);
388
317
  const array = candidate.startsWith("[[");
389
- if (!candidate.startsWith("[") || (array ? !candidate.endsWith("]]") : !candidate.endsWith("]"))) {
318
+ if (
319
+ !candidate.startsWith("[") ||
320
+ (array ? !candidate.endsWith("]]") : !candidate.endsWith("]"))
321
+ ) {
390
322
  return null;
391
323
  }
392
324
  const openWidth = array ? 2 : 1;
@@ -532,7 +464,8 @@ export function stripCodexOpenSocratesTrustSections(contents) {
532
464
  if (
533
465
  pathContainsTrustNamespace(semanticPath) ||
534
466
  ((semanticPath[0]?.value === "hooks" ||
535
- (currentTableSegments[0]?.value === "hooks" && currentTableSegments[1]?.value === "state")) &&
467
+ (currentTableSegments[0]?.value === "hooks" &&
468
+ currentTableSegments[1]?.value === "state")) &&
536
469
  lineContainsTrustNamespaceString(code))
537
470
  ) {
538
471
  trustResetFailure("the matching trust namespace uses a noncanonical assignment shape");
@@ -589,7 +522,12 @@ function currentUid() {
589
522
  }
590
523
 
591
524
  function assertOwnedDirectory(info) {
592
- if (!info.isDirectory() || info.isSymbolicLink() || info.uid !== currentUid() || (info.mode & 0o022) !== 0) {
525
+ if (
526
+ !info.isDirectory() ||
527
+ info.isSymbolicLink() ||
528
+ info.uid !== currentUid() ||
529
+ (info.mode & 0o022) !== 0
530
+ ) {
593
531
  trustResetFailure("the Codex configuration directory is not a safe owner-controlled directory");
594
532
  }
595
533
  }
@@ -756,16 +694,20 @@ function exactCodexAppServerNotification(message) {
756
694
  const keys = Object.keys(message).sort();
757
695
  const withoutTimestamp = keys.length === 2 && keys[0] === "method" && keys[1] === "params";
758
696
  const withTimestamp =
759
- keys.length === 3 &&
760
- keys[0] === "emittedAtMs" &&
761
- keys[1] === "method" &&
762
- keys[2] === "params";
697
+ keys.length === 3 && keys[0] === "emittedAtMs" && keys[1] === "method" && keys[2] === "params";
763
698
  if (!withoutTimestamp && !withTimestamp) return false;
764
699
  if (typeof message.method !== "string" || message.method.length === 0) return false;
765
- if (message.params === null || typeof message.params !== "object" || Array.isArray(message.params)) {
700
+ if (
701
+ message.params === null ||
702
+ typeof message.params !== "object" ||
703
+ Array.isArray(message.params)
704
+ ) {
766
705
  return false;
767
706
  }
768
- return !withTimestamp || (typeof message.emittedAtMs === "number" && Number.isFinite(message.emittedAtMs));
707
+ return (
708
+ !withTimestamp ||
709
+ (typeof message.emittedAtMs === "number" && Number.isFinite(message.emittedAtMs))
710
+ );
769
711
  }
770
712
 
771
713
  function codexAppServerValidationError() {
@@ -1030,7 +972,9 @@ async function validateCodexConfigBytes(
1030
972
  await removeValidationHome(validationHome, { recursive: true, force: true });
1031
973
  } catch {
1032
974
  if (!operationFailed) {
1033
- trustResetFailure("the isolated Codex validation environment could not be removed safely");
975
+ trustResetFailure(
976
+ "the isolated Codex validation environment could not be removed safely",
977
+ );
1034
978
  }
1035
979
  }
1036
980
  }
@@ -1184,7 +1128,9 @@ export async function resetCodexOpenSocratesHookTrust({
1184
1128
  });
1185
1129
  }
1186
1130
  if (error instanceof InstallerError) throw error;
1187
- trustResetFailure("the atomic configuration update failed; the original configuration was preserved");
1131
+ trustResetFailure(
1132
+ "the atomic configuration update failed; the original configuration was preserved",
1133
+ );
1188
1134
  } finally {
1189
1135
  let cleanupFailed = false;
1190
1136
  if (cleanupRequired) {
@@ -1206,6 +1152,23 @@ export async function resetCodexOpenSocratesHookTrust({
1206
1152
 
1207
1153
  export class InstallerError extends Error {}
1208
1154
 
1155
+ class PackagePreparationError extends InstallerError {
1156
+ constructor(outcomes) {
1157
+ const failures = outcomes.filter((item) => item.error !== null);
1158
+ super(
1159
+ "package preparation failed: " +
1160
+ failures
1161
+ .map((item) => `${item.host}: ${String(item.error?.message ?? item.error)}`)
1162
+ .join("; "),
1163
+ );
1164
+ this.hostOutcomes = outcomes.map(({ host, error }) => ({
1165
+ host,
1166
+ result: "failed",
1167
+ errorCategory: error === null ? null : errorCategory(error),
1168
+ }));
1169
+ }
1170
+ }
1171
+
1209
1172
  function fail(message) {
1210
1173
  throw new InstallerError(message);
1211
1174
  }
@@ -1245,7 +1208,9 @@ async function requireRegularFileEntry(target, label) {
1245
1208
  }
1246
1209
 
1247
1210
  export function statePaths() {
1248
- const stateDirectory = resolve(process.env.OPENSOCRATES_STATE_DIR || join(homedir(), ".opensocrates"));
1211
+ const stateDirectory = resolve(
1212
+ process.env.OPENSOCRATES_STATE_DIR || join(homedir(), ".opensocrates"),
1213
+ );
1249
1214
  const launchAgentsDirectory = resolve(
1250
1215
  process.env.OPENSOCRATES_LAUNCH_AGENTS_DIR || join(homedir(), "Library", "LaunchAgents"),
1251
1216
  );
@@ -1281,7 +1246,12 @@ function defaultDesiredState() {
1281
1246
  }
1282
1247
 
1283
1248
  function normalizeDesiredState(value) {
1284
- if (value === null || typeof value !== "object" || Array.isArray(value) || value.schema !== DESIRED_STATE_SCHEMA) {
1249
+ if (
1250
+ value === null ||
1251
+ typeof value !== "object" ||
1252
+ Array.isArray(value) ||
1253
+ value.schema !== DESIRED_STATE_SCHEMA
1254
+ ) {
1285
1255
  fail("the OpenSocrates desired-state manifest has an unsupported schema");
1286
1256
  }
1287
1257
  const installedHosts = value.installedHosts;
@@ -1290,7 +1260,9 @@ function normalizeDesiredState(value) {
1290
1260
  installedHosts.some((host) => !SUPPORTED_HOSTS.includes(host)) ||
1291
1261
  new Set(installedHosts).size !== installedHosts.length
1292
1262
  ) {
1293
- fail("the OpenSocrates desired-state manifest has an invalid host set");
1263
+ fail(
1264
+ "the OpenSocrates desired-state manifest has an invalid or retired host set; uninstall non-Codex hosts using opensocrates@1.3.1 before upgrading",
1265
+ );
1294
1266
  }
1295
1267
  const channel = value.channel;
1296
1268
  if (!new Set(["stable", "next"]).has(channel)) {
@@ -1307,10 +1279,16 @@ function normalizeDesiredState(value) {
1307
1279
  fail("the OpenSocrates desired-state manifest has an invalid update policy");
1308
1280
  }
1309
1281
  const autoUpdateHosts =
1310
- value.autoUpdate.hosts === undefined ? (value.autoUpdate.enabled ? installedHosts : []) : value.autoUpdate.hosts;
1282
+ value.autoUpdate.hosts === undefined
1283
+ ? value.autoUpdate.enabled
1284
+ ? installedHosts
1285
+ : []
1286
+ : value.autoUpdate.hosts;
1311
1287
  if (
1312
1288
  !Array.isArray(autoUpdateHosts) ||
1313
- autoUpdateHosts.some((host) => !SUPPORTED_HOSTS.includes(host) || !installedHosts.includes(host)) ||
1289
+ autoUpdateHosts.some(
1290
+ (host) => !SUPPORTED_HOSTS.includes(host) || !installedHosts.includes(host),
1291
+ ) ||
1314
1292
  new Set(autoUpdateHosts).size !== autoUpdateHosts.length ||
1315
1293
  (value.autoUpdate.enabled && autoUpdateHosts.length === 0) ||
1316
1294
  (!value.autoUpdate.enabled && autoUpdateHosts.length > 0)
@@ -1354,7 +1332,8 @@ async function ensurePrivateDirectory(directory) {
1354
1332
  if (!info.isDirectory() || info.isSymbolicLink()) {
1355
1333
  fail(`refusing to use a non-directory or symbolic-link state path: ${directory}`);
1356
1334
  }
1357
- await chmod(directory, 0o700);
1335
+ if (process.platform === "win32") windowsAction("private", directory);
1336
+ else await chmod(directory, 0o700);
1358
1337
  return;
1359
1338
  }
1360
1339
  await mkdir(directory, { recursive: true, mode: 0o700 });
@@ -1362,7 +1341,8 @@ async function ensurePrivateDirectory(directory) {
1362
1341
  if (!info.isDirectory() || info.isSymbolicLink()) {
1363
1342
  fail(`refusing to use an unsafe state path: ${directory}`);
1364
1343
  }
1365
- await chmod(directory, 0o700);
1344
+ if (process.platform === "win32") windowsAction("private", directory);
1345
+ else await chmod(directory, 0o700);
1366
1346
  }
1367
1347
 
1368
1348
  async function atomicWritePrivateFile(target, contents) {
@@ -1375,7 +1355,8 @@ async function atomicWritePrivateFile(target, contents) {
1375
1355
  mode: 0o600,
1376
1356
  flag: "wx",
1377
1357
  });
1378
- await chmod(temporary, 0o600);
1358
+ if (process.platform === "win32") windowsAction("private", temporary);
1359
+ else await chmod(temporary, 0o600);
1379
1360
  await rename(temporary, target);
1380
1361
  } finally {
1381
1362
  if (await exists(temporary)) {
@@ -1386,7 +1367,10 @@ async function atomicWritePrivateFile(target, contents) {
1386
1367
 
1387
1368
  export async function writeDesiredState(value) {
1388
1369
  const normalized = normalizeDesiredState(value);
1389
- await atomicWritePrivateFile(statePaths().desiredState, `${JSON.stringify(normalized, null, 2)}\n`);
1370
+ await atomicWritePrivateFile(
1371
+ statePaths().desiredState,
1372
+ `${JSON.stringify(normalized, null, 2)}\n`,
1373
+ );
1390
1374
  return normalized;
1391
1375
  }
1392
1376
 
@@ -1397,9 +1381,10 @@ async function writeAutoUpdateReceipt({ version, checkedAt, hosts, result, error
1397
1381
  checkedAt,
1398
1382
  hosts: [...hosts]
1399
1383
  .sort((left, right) => left.host.localeCompare(right.host))
1400
- .map(({ host, result: hostResult }) => ({
1384
+ .map(({ host, result: hostResult, errorCategory: hostErrorCategory }) => ({
1401
1385
  host,
1402
1386
  result: hostResult,
1387
+ errorCategory: hostErrorCategory ?? null,
1403
1388
  })),
1404
1389
  result,
1405
1390
  errorCategory: errorCategory ?? null,
@@ -1414,7 +1399,9 @@ function nowIso() {
1414
1399
  function nextCheckAt(intervalHours) {
1415
1400
  const intervalMilliseconds = intervalHours * 60 * 60 * 1000;
1416
1401
  const jitterBasisPoints = randomInt(-1500, 1501);
1417
- return new Date(Date.now() + intervalMilliseconds * (1 + jitterBasisPoints / 10_000)).toISOString();
1402
+ return new Date(
1403
+ Date.now() + intervalMilliseconds * (1 + jitterBasisPoints / 10_000),
1404
+ ).toISOString();
1418
1405
  }
1419
1406
 
1420
1407
  function majorVersion(value) {
@@ -1422,11 +1409,24 @@ function majorVersion(value) {
1422
1409
  return match ? Number(match[1]) : null;
1423
1410
  }
1424
1411
 
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";
1412
+ export function errorCategory(error) {
1413
+ if (error instanceof PackagePreparationError) {
1414
+ const categories = new Set(
1415
+ error.hostOutcomes.map((item) => item.errorCategory).filter((item) => item !== null),
1416
+ );
1417
+ return categories.size === 1 ? [...categories][0] : "multiple";
1418
+ }
1419
+ const message =
1420
+ error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
1421
+ if (
1422
+ /checksum|manifest|archive|symbolic link|target metadata|runtime layout|unexpected native runtime or launcher surface/u.test(
1423
+ message,
1424
+ )
1425
+ )
1426
+ return "verification";
1428
1427
  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";
1428
+ if (/preflight|not logged|auth|version is required|could not run/u.test(message))
1429
+ return "preflight";
1430
1430
  if (/rollback|restore/u.test(message)) return "rollback";
1431
1431
  if (/lock|already running/u.test(message)) return "locked";
1432
1432
  if (/register|plugin|marketplace/u.test(message)) return "activation";
@@ -1440,7 +1440,10 @@ async function acquireOperationLock() {
1440
1440
  try {
1441
1441
  const handle = await open(paths.lock, "wx", 0o600);
1442
1442
  try {
1443
- await handle.writeFile(`${JSON.stringify({ pid: process.pid, startedAt: nowIso() })}\n`, "utf8");
1443
+ await handle.writeFile(
1444
+ `${JSON.stringify({ pid: process.pid, startedAt: nowIso() })}\n`,
1445
+ "utf8",
1446
+ );
1444
1447
  } catch (writeError) {
1445
1448
  await handle.close().catch(() => undefined);
1446
1449
  await rm(paths.lock, { force: true }).catch(() => undefined);
@@ -1472,7 +1475,9 @@ async function acquireOperationLock() {
1472
1475
  }
1473
1476
  stale =
1474
1477
  ownerAlive === false ||
1475
- (ownerAlive === null && Number.isFinite(started) && Date.now() - started > LOCK_STALE_MILLISECONDS);
1478
+ (ownerAlive === null &&
1479
+ Number.isFinite(started) &&
1480
+ Date.now() - started > LOCK_STALE_MILLISECONDS);
1476
1481
  } catch {
1477
1482
  const info = await lstat(paths.lock);
1478
1483
  stale = Date.now() - info.mtimeMs > LOCK_STALE_MILLISECONDS;
@@ -1514,34 +1519,29 @@ function jsonDeepEqual(left, right) {
1514
1519
  left.every((item, index) => jsonDeepEqual(item, right[index]))
1515
1520
  );
1516
1521
  }
1517
- if (
1518
- left === null ||
1519
- right === null ||
1520
- typeof left !== "object" ||
1521
- typeof right !== "object"
1522
- ) {
1522
+ if (left === null || right === null || typeof left !== "object" || typeof right !== "object") {
1523
1523
  return false;
1524
1524
  }
1525
1525
  const leftKeys = Object.keys(left).sort();
1526
1526
  const rightKeys = Object.keys(right).sort();
1527
1527
  return (
1528
1528
  leftKeys.length === rightKeys.length &&
1529
- leftKeys.every(
1530
- (key, index) => key === rightKeys[index] && jsonDeepEqual(left[key], right[key]),
1531
- )
1529
+ leftKeys.every((key, index) => key === rightKeys[index] && jsonDeepEqual(left[key], right[key]))
1532
1530
  );
1533
1531
  }
1534
1532
 
1535
1533
  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;
1534
+ return CODEX_MARKER;
1541
1535
  }
1542
1536
 
1543
1537
  export function markerMatches(value, host = DEFAULT_HOST) {
1544
- return value !== null && typeof value === "object" && !Array.isArray(value) && jsonEqual(value, markerFor(host));
1538
+ if (host !== "codex") return false;
1539
+ return (
1540
+ value !== null &&
1541
+ typeof value === "object" &&
1542
+ !Array.isArray(value) &&
1543
+ jsonEqual(value, markerFor(host))
1544
+ );
1545
1545
  }
1546
1546
 
1547
1547
  export function isSafeArchivePath(value) {
@@ -1558,7 +1558,17 @@ export function isSafeArchivePath(value) {
1558
1558
  if (candidate.length === 0) {
1559
1559
  return false;
1560
1560
  }
1561
- return candidate.split("/").every((part) => part.length > 0 && part !== "." && part !== "..");
1561
+ return candidate
1562
+ .split("/")
1563
+ .every(
1564
+ (part) =>
1565
+ part.length > 0 &&
1566
+ part !== "." &&
1567
+ part !== ".." &&
1568
+ !/[\x00-\x1f<>:"|?*]/u.test(part) &&
1569
+ !/[. ]$/u.test(part) &&
1570
+ !/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/iu.test(part),
1571
+ );
1562
1572
  }
1563
1573
 
1564
1574
  export function parseChecksumText(text, expectedName = ASSET_NAME) {
@@ -1581,7 +1591,15 @@ export function parseChecksumText(text, expectedName = ASSET_NAME) {
1581
1591
  }
1582
1592
 
1583
1593
  export function parseCli(argv) {
1584
- const actions = new Set(["install", "status", "update", "remove", "verify", "auto-update", "help"]);
1594
+ const actions = new Set([
1595
+ "install",
1596
+ "status",
1597
+ "update",
1598
+ "remove",
1599
+ "verify",
1600
+ "auto-update",
1601
+ "help",
1602
+ ]);
1585
1603
  const args = [...argv];
1586
1604
  let action = "install";
1587
1605
  if (args[0] && !args[0].startsWith("-")) {
@@ -1608,7 +1626,9 @@ export function parseCli(argv) {
1608
1626
  host: action === "auto-update" ? ALL_HOST : DEFAULT_HOST,
1609
1627
  asset: null,
1610
1628
  checksum: null,
1611
- hostAssets: Object.fromEntries(SUPPORTED_HOSTS.map((host) => [host, { asset: null, checksum: null }])),
1629
+ hostAssets: Object.fromEntries(
1630
+ SUPPORTED_HOSTS.map((host) => [host, { asset: null, checksum: null }]),
1631
+ ),
1612
1632
  channel: "stable",
1613
1633
  intervalHours: AUTO_UPDATE_DEFAULT_INTERVAL_HOURS,
1614
1634
  allowMajor: false,
@@ -1678,7 +1698,9 @@ export function parseCli(argv) {
1678
1698
  options.intervalHours = value;
1679
1699
  continue;
1680
1700
  }
1681
- const qualified = flag?.match(new RegExp(`^--(asset|checksum)-(${SUPPORTED_HOSTS.join("|")})$`, "u"));
1701
+ const qualified = flag?.match(
1702
+ new RegExp(`^--(asset|checksum)-(${SUPPORTED_HOSTS.join("|")})$`, "u"),
1703
+ );
1682
1704
  if (qualified) {
1683
1705
  seenOptions.add(qualified[1]);
1684
1706
  const value = args.shift();
@@ -1710,7 +1732,10 @@ export function parseCli(argv) {
1710
1732
  if (options.host === ALL_HOST && options.asset !== null) {
1711
1733
  fail("--host all requires host-qualified --asset-<host> and --checksum-<host> options");
1712
1734
  }
1713
- if (options.host !== ALL_HOST && SUPPORTED_HOSTS.some((host) => options.hostAssets[host].asset !== null)) {
1735
+ if (
1736
+ options.host !== ALL_HOST &&
1737
+ SUPPORTED_HOSTS.some((host) => options.hostAssets[host].asset !== null)
1738
+ ) {
1714
1739
  fail("host-qualified asset options are only valid with --host all");
1715
1740
  }
1716
1741
  if (["status", "remove", "help"].includes(options.action) && options.asset !== null) {
@@ -1725,11 +1750,13 @@ export function parseCli(argv) {
1725
1750
  if (
1726
1751
  options.action === "auto-update" &&
1727
1752
  options.autoUpdateAction !== "run" &&
1728
- (options.asset !== null || SUPPORTED_HOSTS.some((host) => options.hostAssets[host].asset !== null))
1753
+ (options.asset !== null ||
1754
+ SUPPORTED_HOSTS.some((host) => options.hostAssets[host].asset !== null))
1729
1755
  ) {
1730
1756
  fail(`auto-update ${options.autoUpdateAction} does not accept local asset options`);
1731
1757
  }
1732
- const autoUpdateEnable = options.action === "auto-update" && options.autoUpdateAction === "enable";
1758
+ const autoUpdateEnable =
1759
+ options.action === "auto-update" && options.autoUpdateAction === "enable";
1733
1760
  const autoUpdateRun = options.action === "auto-update" && options.autoUpdateAction === "run";
1734
1761
  for (const policyOption of ["allow-major", "channel", "interval-hours"]) {
1735
1762
  if (seenOptions.has(policyOption) && !autoUpdateEnable) {
@@ -1765,12 +1792,12 @@ function showHelp() {
1765
1792
  console.log(`OpenSocrates ${PRODUCT_VERSION}
1766
1793
 
1767
1794
  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]
1795
+ opensocrates install [--host codex|all] [--asset ZIP --checksum FILE]
1796
+ opensocrates status [--host codex|all]
1797
+ opensocrates update [--host codex|all] [--asset ZIP --checksum FILE]
1798
+ opensocrates remove [--host codex|all] [--purge [--reset-trust]]
1799
+ opensocrates verify [--host codex|all] [--asset ZIP --checksum FILE]
1800
+ opensocrates auto-update enable [--host codex|all]
1774
1801
  [--channel stable|next] [--interval-hours ${AUTO_UPDATE_DEFAULT_INTERVAL_HOURS}]
1775
1802
  [--allow-major]
1776
1803
  opensocrates auto-update status
@@ -1794,28 +1821,9 @@ reported as pending.
1794
1821
  }
1795
1822
 
1796
1823
  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");
1824
+ if (host !== "codex") fail("only Codex is supported");
1825
+ const configured = process.env.CODEX_HOME;
1826
+ const defaultHome = join(homedir(), ".codex");
1819
1827
  const configuredHome = resolve(configured ? configured : defaultHome);
1820
1828
  let hostHome = configuredHome;
1821
1829
  try {
@@ -1825,27 +1833,21 @@ function managedPaths(host) {
1825
1833
  // explicit, non-recursive target. Lexical resolution is the only
1826
1834
  // available normalization until then.
1827
1835
  }
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);
1836
+ const root = join(hostHome, "managed-marketplaces", MARKETPLACE_NAME);
1838
1837
  const layout = HOST_LAYOUTS[host];
1839
1838
  return {
1840
1839
  hostHome,
1841
1840
  root,
1842
1841
  parent: dirname(root),
1843
1842
  marker: join(root, MARKER_NAME),
1844
- marketplace: typeof layout.marketplaceRelative === "string" ? join(root, layout.marketplaceRelative) : null,
1843
+ marketplace:
1844
+ typeof layout.marketplaceRelative === "string"
1845
+ ? join(root, layout.marketplaceRelative)
1846
+ : null,
1845
1847
  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,
1848
+ bridge: null,
1849
+ bridgeMarker: null,
1850
+ bridgeParent: null,
1849
1851
  };
1850
1852
  }
1851
1853
 
@@ -1854,32 +1856,19 @@ export function purgePathsFor(host) {
1854
1856
  fail(`unsupported host ${JSON.stringify(host)}`);
1855
1857
  }
1856
1858
  const paths = managedPaths(host);
1857
- const cacheRoot = ["claude", "codex"].includes(host)
1858
- ? join(paths.hostHome, "plugins", "cache", MARKETPLACE_NAME, PLUGIN_NAME)
1859
- : null;
1859
+ const cacheRoot = join(paths.hostHome, "plugins", "cache", MARKETPLACE_NAME, PLUGIN_NAME);
1860
1860
  return {
1861
1861
  ...paths,
1862
1862
  cacheRoot,
1863
1863
  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
- : [],
1864
+ pluginData: [],
1871
1865
  };
1872
1866
  }
1873
1867
 
1874
1868
  // Directory that holds staging trees and rollback backups for one host.
1875
1869
  //
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
1870
  function transientParent(host, paths) {
1882
- return host === "grok" ? paths.hostHome : paths.parent;
1871
+ return paths.parent;
1883
1872
  }
1884
1873
 
1885
1874
  export function transientPathsFor(host) {
@@ -1891,34 +1880,40 @@ function codexBinary() {
1891
1880
  return process.env.CODEX_BIN || "codex";
1892
1881
  }
1893
1882
 
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";
1883
+ function windowsAction(action, target, destination = "") {
1884
+ const result = spawnSync(
1885
+ "powershell.exe",
1886
+ [
1887
+ "-NoProfile",
1888
+ "-NonInteractive",
1889
+ "-ExecutionPolicy",
1890
+ "Bypass",
1891
+ "-File",
1892
+ fileURLToPath(new URL("./windows.ps1", import.meta.url)),
1893
+ "-Action",
1894
+ action,
1895
+ ],
1896
+ {
1897
+ encoding: "utf8",
1898
+ windowsHide: true,
1899
+ maxBuffer: 16 * 1024 * 1024,
1900
+ env: {
1901
+ ...process.env,
1902
+ OPENSOCRATES_WINDOWS_PATH: target,
1903
+ OPENSOCRATES_WINDOWS_DESTINATION: destination,
1904
+ },
1905
+ },
1906
+ );
1907
+ if (result.error || result.status !== 0)
1908
+ fail(`Windows ${action} failed: ${result.error?.message || result.stderr?.trim()}`);
1909
+ return result.stdout;
1916
1910
  }
1917
1911
 
1918
1912
  function run(command, args, { allowFailure = false } = {}) {
1919
1913
  const result = spawnSync(command, args, {
1920
1914
  encoding: "utf8",
1921
1915
  env: process.env,
1916
+ windowsHide: true,
1922
1917
  maxBuffer: 16 * 1024 * 1024,
1923
1918
  });
1924
1919
  if (result.error) {
@@ -1958,139 +1953,7 @@ function runCodexJson(args) {
1958
1953
  return payload;
1959
1954
  }
1960
1955
 
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
1956
  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
1957
  run(codexBinary(), ["--version"]);
2095
1958
  if (authenticated) {
2096
1959
  run(codexBinary(), ["login", "status"]);
@@ -2098,17 +1961,8 @@ function requireHostCli(host, { authenticated = false } = {}) {
2098
1961
  }
2099
1962
 
2100
1963
  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;
1964
+ const payload = runCodexJson(["plugin", "marketplace", "list", "--json"]).marketplaces;
1965
+ const entries = payload;
2112
1966
  if (!Array.isArray(entries)) {
2113
1967
  fail(`${host} marketplace list returned an unexpected schema`);
2114
1968
  }
@@ -2124,143 +1978,15 @@ function marketplaceEntry(host) {
2124
1978
  return matches[0] ?? null;
2125
1979
  }
2126
1980
 
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
1981
  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"]);
1982
+ const payload = runCodexJson([
1983
+ "plugin",
1984
+ "list",
1985
+ "--marketplace",
1986
+ MARKETPLACE_NAME,
1987
+ "--available",
1988
+ "--json",
1989
+ ]);
2264
1990
  const installed = Array.isArray(payload.installed) ? payload.installed : null;
2265
1991
  const available = Array.isArray(payload.available) ? payload.available : null;
2266
1992
  if (installed === null || available === null) {
@@ -2286,51 +2012,10 @@ function pluginState(host, { requireHostState = false } = {}) {
2286
2012
  return { kind: "missing", version: null };
2287
2013
  }
2288
2014
 
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
2015
  // Install and update write into the managed root, so a case-variant pre-1.0
2306
2016
  // registration must be resolved by the user first. Status and remove are
2307
2017
  // diagnostic and scoped to the root this installer owns, so they only warn:
2308
2018
  // 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
2019
 
2335
2020
  async function readJsonObject(target) {
2336
2021
  let payload;
@@ -2366,85 +2051,6 @@ async function sha256File(target) {
2366
2051
  return hash.digest("hex");
2367
2052
  }
2368
2053
 
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;
2384
- }
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
2054
  async function downloadFile(url, destination) {
2449
2055
  const response = await fetch(url, {
2450
2056
  headers: {
@@ -2509,8 +2115,10 @@ async function verifyOuterChecksum(asset, checksum, host) {
2509
2115
 
2510
2116
  function archiveEntries(asset) {
2511
2117
  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);
2118
+ const entries =
2119
+ process.platform === "win32"
2120
+ ? JSON.parse(windowsAction("entries", asset))
2121
+ : run(unzip, ["-Z1", asset]).stdout.split(/\r?\n/u).filter(Boolean);
2514
2122
  if (entries.length === 0 || entries.length > MAX_ARCHIVE_ENTRIES) {
2515
2123
  fail(`archive contains an invalid number of entries: ${entries.length}`);
2516
2124
  }
@@ -2531,7 +2139,8 @@ async function extractArchive(asset, destination) {
2531
2139
  archiveEntries(asset);
2532
2140
  await mkdir(destination, { recursive: true, mode: 0o700 });
2533
2141
  const unzip = process.platform === "darwin" ? "/usr/bin/unzip" : "unzip";
2534
- run(unzip, ["-q", asset, "-d", destination]);
2142
+ if (process.platform === "win32") windowsAction("extract", asset, destination);
2143
+ else run(unzip, ["-q", asset, "-d", destination]);
2535
2144
  }
2536
2145
 
2537
2146
  async function walkFiles(root, current = root, output = []) {
@@ -2580,13 +2189,23 @@ async function verifyPackageChecksums(pluginRoot) {
2580
2189
  fail(`package checksum mismatch for ${item}`);
2581
2190
  }
2582
2191
  }
2583
- const actualFiles = new Set((await walkFiles(pluginRoot)).filter((item) => item !== "checksums.sha256"));
2192
+ const actualFiles = new Set(
2193
+ (await walkFiles(pluginRoot)).filter((item) => item !== "checksums.sha256"),
2194
+ );
2584
2195
  if (actualFiles.size !== declared.size || [...actualFiles].some((item) => !declared.has(item))) {
2585
2196
  fail("package checksum manifest does not cover the complete archive");
2586
2197
  }
2587
2198
  return declared.size;
2588
2199
  }
2589
2200
 
2201
+ function hasExactStringEntries(value, expected) {
2202
+ return (
2203
+ Array.isArray(value) &&
2204
+ value.length === expected.length &&
2205
+ value.every((item, index) => item === expected[index])
2206
+ );
2207
+ }
2208
+
2590
2209
  async function verifyExtractedPackage(pluginRoot, host) {
2591
2210
  const manifest = await readJsonObject(join(pluginRoot, HOST_LAYOUTS[host].manifestRelative));
2592
2211
  if (manifest.name !== PLUGIN_NAME || manifest.version !== PRODUCT_VERSION) {
@@ -2604,37 +2223,57 @@ async function verifyExtractedPackage(pluginRoot, host) {
2604
2223
  fail("package release manifest does not match this installer");
2605
2224
  }
2606
2225
  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");
2611
- }
2612
- } else if (
2613
- release.launchers?.length !== 0 ||
2614
- release.runtime_targets?.length !== 0 ||
2615
- (await exists(join(pluginRoot, "runtime"))) ||
2616
- (await exists(join(pluginRoot, "hooks"))) ||
2617
- (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")))))
2623
- ) {
2624
- fail(`${host} package contains an unexpected native runtime or launcher surface`);
2625
- }
2626
- if (host === "opencode") {
2226
+ const target = process.platform === "win32" ? "windows-x64" : "darwin-arm64";
2227
+ const runtimeRoot = join(pluginRoot, "runtime");
2228
+ const executable =
2229
+ process.platform === "win32" ? "opensocrates-runtime.exe" : "opensocrates-runtime";
2230
+ const runtime = join(runtimeRoot, target, "opensocrates-runtime", executable);
2627
2231
  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")))
2232
+ !hasExactStringEntries(release.runtime_targets, [target]) ||
2233
+ !hasExactStringEntries(release.release_targets, [target])
2633
2234
  ) {
2634
- fail("OpenCode package does not match the verified stable bridge contract");
2235
+ fail(`package target metadata does not declare only ${target}`);
2635
2236
  }
2636
- }
2637
- return verifyPackageChecksums(pluginRoot);
2237
+ let packagedTargets;
2238
+ try {
2239
+ packagedTargets = await readdir(runtimeRoot, { withFileTypes: true });
2240
+ } catch {
2241
+ fail(`package runtime layout cannot be inspected for ${target}`);
2242
+ }
2243
+ if (
2244
+ packagedTargets.length !== 1 ||
2245
+ packagedTargets[0].name !== target ||
2246
+ !packagedTargets[0].isDirectory() ||
2247
+ packagedTargets[0].isSymbolicLink()
2248
+ ) {
2249
+ fail(`package runtime layout does not contain only ${target}`);
2250
+ }
2251
+ let runtimeInfo;
2252
+ try {
2253
+ runtimeInfo = await stat(runtime);
2254
+ } catch {
2255
+ fail(`package runtime layout is missing the executable ${target} runtime`);
2256
+ }
2257
+ if (
2258
+ !runtimeInfo.isFile() ||
2259
+ (process.platform !== "win32" && (runtimeInfo.mode & 0o111) === 0)
2260
+ ) {
2261
+ fail(`package runtime layout is missing the executable ${target} runtime`);
2262
+ }
2263
+ } else if (
2264
+ !hasExactStringEntries(release.release_targets, []) ||
2265
+ !hasExactStringEntries(release.launchers, []) ||
2266
+ !hasExactStringEntries(release.runtime_targets, []) ||
2267
+ (await exists(join(pluginRoot, "runtime"))) ||
2268
+ (await exists(join(pluginRoot, "hooks"))) ||
2269
+ (await exists(join(pluginRoot, "bin"))) ||
2270
+ false ||
2271
+ false
2272
+ ) {
2273
+ fail(`${host} package contains an unexpected native runtime or launcher surface`);
2274
+ }
2275
+
2276
+ return verifyPackageChecksums(pluginRoot);
2638
2277
  }
2639
2278
 
2640
2279
  async function requireSafePathBelow(root, target, label, { allowRoot = false } = {}) {
@@ -2725,7 +2364,8 @@ async function packageIdentityForPurge(pluginRoot, host, { allowedExtra = () =>
2725
2364
  fail(`OpenSocrates ${host} payload has an unsafe checksum path`);
2726
2365
  }
2727
2366
  const target = join(pluginRoot, ...item.split("/"));
2728
- if (!(await entryExists(target))) fail(`OpenSocrates ${host} payload is missing a declared file`);
2367
+ if (!(await entryExists(target)))
2368
+ fail(`OpenSocrates ${host} payload is missing a declared file`);
2729
2369
  const info = await lstat(target);
2730
2370
  if (!info.isFile() || info.isSymbolicLink()) {
2731
2371
  fail(`OpenSocrates ${host} payload contains an unsafe declared file`);
@@ -2788,7 +2428,11 @@ async function purgeHostCache(host, paths) {
2788
2428
  if (!(await entryExists(paths.cacheRoot))) {
2789
2429
  let removedEmptyMarketplace = false;
2790
2430
  if (await entryExists(paths.cacheMarketplaceRoot)) {
2791
- await requireSafePathBelow(paths.hostHome, paths.cacheMarketplaceRoot, `${host} cache marketplace`);
2431
+ await requireSafePathBelow(
2432
+ paths.hostHome,
2433
+ paths.cacheMarketplaceRoot,
2434
+ `${host} cache marketplace`,
2435
+ );
2792
2436
  const info = await lstat(paths.cacheMarketplaceRoot);
2793
2437
  if (!info.isDirectory() || info.isSymbolicLink()) {
2794
2438
  fail(`refusing an unsafe ${host} OpenSocrates cache marketplace`);
@@ -2860,90 +2504,30 @@ async function purgeHostCache(host, paths) {
2860
2504
  };
2861
2505
  }
2862
2506
 
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
2507
  async function verifyManagedTreeForPurge(host, root) {
2926
- if (host === "opencode") return verifyOpenCodePurgeRoot(root);
2927
2508
  await requireOwnedRoot(root, host);
2928
2509
  const layout = HOST_LAYOUTS[host];
2929
2510
  const pluginRoot = join(root, layout.pluginRelative);
2930
2511
  const identity = await packageIdentityForPurge(pluginRoot, host, {
2931
- allowedExtra: (item) => FILE_DROP_HOSTS.includes(host) && item === MARKER_NAME,
2512
+ allowedExtra: (item) => false,
2932
2513
  });
2933
- if (FILE_DROP_HOSTS.includes(host)) return identity;
2934
2514
 
2935
2515
  const marketplacePath = join(root, layout.marketplaceRelative);
2936
2516
  await requireRegularFileEntry(marketplacePath, `${host} marketplace manifest`);
2937
2517
  const marketplace = await readJsonObject(marketplacePath);
2938
2518
  const expected = expectedMarketplace(host);
2939
- if (host === "claude") expected.metadata.version = identity.version;
2519
+
2940
2520
  if (!jsonDeepEqual(marketplace, expected)) {
2941
2521
  fail(`${host} managed marketplace does not match the exact owned plugin inventory`);
2942
2522
  }
2943
2523
  const allowed = new Set([
2944
2524
  MARKER_NAME,
2945
2525
  layout.marketplaceRelative.split(sep).join("/"),
2946
- ...identity.files.map((item) => join(layout.pluginRelative, ...item.split("/")).split(sep).join("/")),
2526
+ ...identity.files.map((item) =>
2527
+ join(layout.pluginRelative, ...item.split("/"))
2528
+ .split(sep)
2529
+ .join("/"),
2530
+ ),
2947
2531
  ]);
2948
2532
  for (const item of await walkFiles(root)) {
2949
2533
  if (!allowed.has(item)) fail(`${host} managed marketplace contains an unowned file: ${item}`);
@@ -2965,10 +2549,13 @@ async function removeOwnedManagedRoot(host, paths) {
2965
2549
  await rm(backup, { recursive: true });
2966
2550
  } catch (error) {
2967
2551
  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
- });
2552
+ const restored = await recoveryStep(
2553
+ `restore the ${host} managed root after purge cleanup failed`,
2554
+ async () => {
2555
+ await verifyManagedTreeForPurge(host, backup);
2556
+ await rename(backup, paths.root);
2557
+ },
2558
+ );
2972
2559
  if (!restored) {
2973
2560
  console.error(`error: recoverable ${host} files remain at: ${backup}`);
2974
2561
  console.error(`error: retry command: opensocrates remove --host ${host} --purge`);
@@ -2984,7 +2571,9 @@ async function cleanupTransientRootResidue(host, paths) {
2984
2571
  if (!(await entryExists(parent))) {
2985
2572
  return { component: "transaction-residue", status: "absent", path: parent };
2986
2573
  }
2987
- await requireSafePathBelow(paths.hostHome, parent, `${host} transient parent`, { allowRoot: true });
2574
+ await requireSafePathBelow(paths.hostHome, parent, `${host} transient parent`, {
2575
+ allowRoot: true,
2576
+ });
2988
2577
  const names = (await readdir(parent)).filter((name) =>
2989
2578
  /^\.opensocrates\.(?:staging|backup|removed)-[A-Za-z0-9-]+$/u.test(name),
2990
2579
  );
@@ -3014,97 +2603,6 @@ async function cleanupTransientRootResidue(host, paths) {
3014
2603
  };
3015
2604
  }
3016
2605
 
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
2606
  async function prepareVerifiedPackage(options, host = options.host) {
3109
2607
  if (!SUPPORTED_HOSTS.includes(host)) {
3110
2608
  fail(`cannot prepare a package for unsupported host ${JSON.stringify(host)}`);
@@ -3124,35 +2622,25 @@ async function prepareVerifiedPackage(options, host = options.host) {
3124
2622
  }
3125
2623
 
3126
2624
  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) {
2625
+ const settled = await Promise.allSettled(
2626
+ hosts.map((host) => prepareVerifiedPackage(options, host)),
2627
+ );
2628
+ const prepared = settled
2629
+ .filter((result) => result.status === "fulfilled")
2630
+ .map((result) => result.value);
2631
+ if (settled.some((result) => result.status === "rejected")) {
3131
2632
  await Promise.all(prepared.map((item) => rm(item.scratch, { recursive: true, force: true })));
3132
- throw failure.reason;
2633
+ throw new PackagePreparationError(
2634
+ settled.map((result, index) => ({
2635
+ host: hosts[index],
2636
+ error: result.status === "rejected" ? result.reason : null,
2637
+ })),
2638
+ );
3133
2639
  }
3134
2640
  return prepared;
3135
2641
  }
3136
2642
 
3137
2643
  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
2644
  return {
3157
2645
  name: MARKETPLACE_NAME,
3158
2646
  interface: { displayName: "OpenSocrates" },
@@ -3177,15 +2665,6 @@ async function buildStagingTree(parent, pluginSource, host) {
3177
2665
  const layout = HOST_LAYOUTS[host];
3178
2666
  const staging = await mkdtemp(join(parent, ".opensocrates.staging-"));
3179
2667
  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
2668
  const marketplace = join(staging, layout.marketplaceRelative);
3190
2669
  const plugin = join(staging, layout.pluginRelative);
3191
2670
  await mkdir(dirname(marketplace), { recursive: true, mode: 0o700 });
@@ -3210,88 +2689,12 @@ async function buildStagingTree(parent, pluginSource, host) {
3210
2689
  }
3211
2690
  }
3212
2691
 
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
2692
  function entryRoot(entry, host) {
3270
2693
  if (entry === null) {
3271
2694
  return null;
3272
2695
  }
3273
2696
  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 {
2697
+ {
3295
2698
  value = entry.root;
3296
2699
  }
3297
2700
  if (typeof value !== "string" || value.trim().length === 0) {
@@ -3310,16 +2713,6 @@ function entryRoot(entry, host) {
3310
2713
  }
3311
2714
 
3312
2715
  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
2716
  if (state.kind === "installed") {
3324
2717
  runCodexJson(["plugin", "remove", PLUGIN_ID, "--json"]);
3325
2718
  }
@@ -3329,17 +2722,6 @@ function removeRegistration(host, entry, state) {
3329
2722
  }
3330
2723
 
3331
2724
  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
2725
  runCodexJson(["plugin", "marketplace", "add", root, "--json"]);
3344
2726
  if (installPlugin) {
3345
2727
  return runCodexJson(["plugin", "add", PLUGIN_ID, "--json"]);
@@ -3348,7 +2730,6 @@ function addRegistration(host, root, installPlugin, { enabled = true } = {}) {
3348
2730
  }
3349
2731
 
3350
2732
  function removeRegistrationBestEffort(host) {
3351
- if (FILE_DROP_HOSTS.includes(host) || host === "opencode") return;
3352
2733
  const entry = (() => {
3353
2734
  try {
3354
2735
  return marketplaceEntry(host);
@@ -3362,21 +2743,15 @@ function removeRegistrationBestEffort(host) {
3362
2743
  try {
3363
2744
  const state = pluginState(host);
3364
2745
  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"];
2746
+ const binary = codexBinary();
2747
+ const args = ["plugin", "remove", PLUGIN_ID, "--json"];
3370
2748
  run(binary, args, { allowFailure: true });
3371
2749
  }
3372
2750
  } catch {
3373
2751
  // Rollback continues with marketplace cleanup.
3374
2752
  }
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"];
2753
+ const binary = codexBinary();
2754
+ const args = ["plugin", "marketplace", "remove", MARKETPLACE_NAME, "--json"];
3380
2755
  run(binary, args, { allowFailure: true });
3381
2756
  }
3382
2757
 
@@ -3392,38 +2767,12 @@ async function recoveryStep(label, action) {
3392
2767
  }
3393
2768
 
3394
2769
  async function preflightHost(host, action) {
3395
- if (
3396
- (!FILE_DROP_HOSTS.includes(host) && host !== "opencode") ||
3397
- ["install", "update"].includes(action)
3398
- ) {
2770
+ {
3399
2771
  requireHostCli(host, { authenticated: ["install", "update"].includes(action) });
3400
2772
  }
3401
- if (host === "claude") {
3402
- if (["install", "update"].includes(action)) {
3403
- requireNoLegacyClaudeInstallation();
3404
- } else {
3405
- warnLegacyClaudeInstallation();
3406
- }
3407
- }
2773
+
3408
2774
  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
- }
2775
+
3427
2776
  const previousEntry = marketplaceEntry(host);
3428
2777
  if (previousEntry !== null && entryRoot(previousEntry, host) !== paths.root) {
3429
2778
  fail(
@@ -3458,7 +2807,9 @@ async function preflightSelectedHosts(options, action, desiredState) {
3458
2807
  const candidates = assetHosts.length > 0 ? assetHosts : SUPPORTED_HOSTS;
3459
2808
  const desiredHosts = new Set(desiredState.installedHosts);
3460
2809
  const rootPresence = Object.fromEntries(
3461
- await Promise.all(candidates.map(async (host) => [host, await entryExists(managedPaths(host).root)])),
2810
+ await Promise.all(
2811
+ candidates.map(async (host) => [host, await entryExists(managedPaths(host).root)]),
2812
+ ),
3462
2813
  );
3463
2814
  const settled = await Promise.allSettled(candidates.map((host) => preflightHost(host, action)));
3464
2815
  const successes = new Map();
@@ -3523,17 +2874,13 @@ async function stageInstallation(preflight, pluginSource) {
3523
2874
  await mkdir(paths.parent, { recursive: true, mode: 0o700 });
3524
2875
  const transient = transientParent(host, paths);
3525
2876
  await mkdir(transient, { recursive: true, mode: 0o700 });
3526
- const staging =
3527
- host === "opencode"
3528
- ? await buildOpenCodeStaging(paths, pluginSource)
3529
- : await buildStagingTree(transient, pluginSource, host);
2877
+ const staging = await buildStagingTree(transient, pluginSource, host);
3530
2878
  return {
3531
2879
  ...preflight,
3532
2880
  staging,
3533
2881
  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,
2882
+ bridgeBackup: null,
2883
+ bridgeMarkerBackup: null,
3537
2884
  registrationRemoved: false,
3538
2885
  backupCreated: false,
3539
2886
  newRootActive: false,
@@ -3556,23 +2903,10 @@ async function activateInstallation(transaction) {
3556
2903
  await rename(paths.root, transaction.backup);
3557
2904
  transaction.backupCreated = true;
3558
2905
  }
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);
2906
+
2907
+ await rename(transaction.staging, paths.root);
3568
2908
  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
- }
2909
+
3576
2910
  const result = addRegistration(host, paths.root, true);
3577
2911
  const state = pluginState(host, { requireHostState: true });
3578
2912
  if (
@@ -3599,35 +2933,18 @@ async function rollbackInstallation(transaction) {
3599
2933
  await rm(paths.root, { recursive: true, force: true });
3600
2934
  });
3601
2935
  }
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
- }
2936
+
3624
2937
  if (transaction.backupCreated && (await exists(transaction.backup))) {
3625
2938
  restored = await recoveryStep(`restore the previous ${host} installation`, async () => {
3626
2939
  await requireOwnedRoot(transaction.backup, host);
3627
2940
  await rename(transaction.backup, paths.root);
3628
2941
  });
3629
2942
  }
3630
- if (transaction.registrationRemoved && previousEntry !== null && (restored || !transaction.backupCreated)) {
2943
+ if (
2944
+ transaction.registrationRemoved &&
2945
+ previousEntry !== null &&
2946
+ (restored || !transaction.backupCreated)
2947
+ ) {
3631
2948
  await recoveryStep(`re-register the previous ${host} installation`, async () => {
3632
2949
  addRegistration(host, paths.root, ["installed", "disabled"].includes(previousState.kind), {
3633
2950
  enabled: previousState.kind !== "disabled",
@@ -3635,20 +2952,20 @@ async function rollbackInstallation(transaction) {
3635
2952
  });
3636
2953
  }
3637
2954
  if (transaction.backupCreated && !restored) {
3638
- console.error(`error: the previous ${host} OpenSocrates installation could not be restored automatically.`);
2955
+ console.error(
2956
+ `error: the previous ${host} OpenSocrates installation could not be restored automatically.`,
2957
+ );
3639
2958
  console.error(`error: your previous files are preserved at: ${transaction.backup}`);
3640
2959
  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)}`);
2960
+ console.error(
2961
+ `error: recovery command: /bin/mv -- ${shellQuote(transaction.backup)} ${shellQuote(paths.root)}`,
2962
+ );
3642
2963
  console.error(`error: recovery command: opensocrates install --host ${host}`);
3643
2964
  }
3644
2965
  }
3645
2966
 
3646
2967
  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)) {
2968
+ if (await exists(transaction.staging)) {
3652
2969
  await rm(transaction.staging, { recursive: true, force: true });
3653
2970
  }
3654
2971
  }
@@ -3659,14 +2976,6 @@ async function commitInstallation(transaction) {
3659
2976
  await rm(transaction.backup, { recursive: true });
3660
2977
  transaction.backupCreated = false;
3661
2978
  }
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
2979
  }
3671
2980
 
3672
2981
  function installedDesiredState(previous, hosts) {
@@ -3689,7 +2998,8 @@ async function runInstallOrUpdate(options, action) {
3689
2998
  for (const preflight of preflights) {
3690
2999
  const item = packages.get(preflight.host);
3691
3000
  console.log(
3692
- `${preflight.host}: verified OpenSocrates ${PRODUCT_VERSION} and ` + `${item.checkedFiles} package files`,
3001
+ `${preflight.host}: verified OpenSocrates ${PRODUCT_VERSION} and ` +
3002
+ `${item.checkedFiles} package files`,
3693
3003
  );
3694
3004
  transactions.push(await stageInstallation(preflight, item.pluginRoot));
3695
3005
  }
@@ -3708,9 +3018,12 @@ async function runInstallOrUpdate(options, action) {
3708
3018
  await Promise.all(prepared.map((item) => rm(item.scratch, { recursive: true, force: true })));
3709
3019
  }
3710
3020
  for (const transaction of transactions) {
3711
- const cleaned = await recoveryStep(`remove the committed ${transaction.host} backup`, async () => {
3712
- await commitInstallation(transaction);
3713
- });
3021
+ const cleaned = await recoveryStep(
3022
+ `remove the committed ${transaction.host} backup`,
3023
+ async () => {
3024
+ await commitInstallation(transaction);
3025
+ },
3026
+ );
3714
3027
  if (!cleaned) {
3715
3028
  console.warn(`warning: ${transaction.host} is active, but its previous backup needs cleanup`);
3716
3029
  }
@@ -3724,39 +3037,16 @@ async function runInstallOrUpdate(options, action) {
3724
3037
  "codex exec silently skips hooks that have not been trusted.",
3725
3038
  );
3726
3039
  }
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
- }
3040
+
3744
3041
  console.log("Start new host tasks to load the updated OpenSocrates integration.");
3745
3042
  return hosts;
3746
3043
  }
3747
3044
 
3748
3045
  async function inspectHostStatus(host) {
3749
- if (!FILE_DROP_HOSTS.includes(host) && host !== "opencode") requireHostCli(host);
3750
- if (host === "claude") warnLegacyClaudeInstallation();
3046
+ requireHostCli(host);
3047
+
3751
3048
  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
- }
3049
+
3760
3050
  const entry = marketplaceEntry(host);
3761
3051
  if (entry === null) {
3762
3052
  if (await exists(paths.root)) {
@@ -3787,19 +3077,15 @@ async function showStatus(host) {
3787
3077
  const status = await inspectHostStatus(candidate);
3788
3078
  const expected = desired.installedHosts.includes(candidate);
3789
3079
  const hostDrift = expected
3790
- ? status.kind !== "installed" || desired.activeVersion === null || status.version !== desired.activeVersion
3080
+ ? status.kind !== "installed" ||
3081
+ desired.activeVersion === null ||
3082
+ status.version !== desired.activeVersion
3791
3083
  : status.kind !== "missing";
3792
3084
  drift ||= hostDrift;
3793
3085
  if (status.kind === "installed") {
3794
3086
  console.log(
3795
3087
  `${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
- : "") +
3088
+ "" +
3803
3089
  (expected
3804
3090
  ? hostDrift
3805
3091
  ? ` (drift from ${desired.activeVersion ?? "desired state"})`
@@ -3819,7 +3105,9 @@ async function showStatus(host) {
3819
3105
  } else if (status.kind === "files-only") {
3820
3106
  console.log(`${candidate}: managed files present but not registered`);
3821
3107
  } else {
3822
- console.log(`${candidate}: not installed` + (expected ? " (drift: desired host is missing)" : ""));
3108
+ console.log(
3109
+ `${candidate}: not installed` + (expected ? " (drift: desired host is missing)" : ""),
3110
+ );
3823
3111
  }
3824
3112
  } catch (error) {
3825
3113
  if (desired.installedHosts.includes(candidate)) drift = true;
@@ -3834,24 +3122,11 @@ async function showStatus(host) {
3834
3122
  }
3835
3123
  const status = await inspectHostStatus(host);
3836
3124
  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
- );
3125
+ console.log(`OpenSocrates ${status.version ?? "unknown"} is installed.` + "");
3849
3126
  } else if (status.kind === "disabled") {
3850
3127
  console.log(
3851
3128
  `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."),
3129
+ "Run install or update to re-enable it.",
3855
3130
  );
3856
3131
  } else if (status.kind === "available") {
3857
3132
  console.log(`OpenSocrates ${status.version ?? "unknown"} is available but not installed.`);
@@ -3869,14 +3144,8 @@ function removalTransaction(preflight) {
3869
3144
  transientParent(preflight.host, preflight.paths),
3870
3145
  `.opensocrates.removed-${randomUUID()}`,
3871
3146
  ),
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,
3147
+ bridgeBackup: null,
3148
+ bridgeMarkerBackup: null,
3880
3149
  registrationRemoved: false,
3881
3150
  backupCreated: false,
3882
3151
  rootCommitted: false,
@@ -3897,20 +3166,16 @@ async function activateRemoval(transaction) {
3897
3166
  await rename(paths.root, transaction.backup);
3898
3167
  transaction.backupCreated = true;
3899
3168
  }
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
3169
  }
3909
3170
 
3910
3171
  async function rollbackRemoval(transaction) {
3911
3172
  let complete = true;
3912
3173
  let restored = !transaction.rootExists;
3913
- if (transaction.rootExists && transaction.backupCreated && (await entryExists(transaction.backup))) {
3174
+ if (
3175
+ transaction.rootExists &&
3176
+ transaction.backupCreated &&
3177
+ (await entryExists(transaction.backup))
3178
+ ) {
3914
3179
  restored = await recoveryStep(`restore the removed ${transaction.host} files`, async () => {
3915
3180
  await verifyManagedTreeForPurge(transaction.host, transaction.backup);
3916
3181
  await rename(transaction.backup, transaction.paths.root);
@@ -3920,7 +3185,10 @@ async function rollbackRemoval(transaction) {
3920
3185
  try {
3921
3186
  await rename(transaction.paths.root, transaction.backup);
3922
3187
  } catch (requarantineError) {
3923
- const detail = requarantineError instanceof Error ? requarantineError.message : String(requarantineError);
3188
+ const detail =
3189
+ requarantineError instanceof Error
3190
+ ? requarantineError.message
3191
+ : String(requarantineError);
3924
3192
  transaction.backupCreated = await entryExists(transaction.backup);
3925
3193
  fail(
3926
3194
  `the restored ${transaction.host} tree failed verification and could not be returned to ` +
@@ -3943,52 +3211,22 @@ async function rollbackRemoval(transaction) {
3943
3211
  restored &&
3944
3212
  (await entryExists(transaction.paths.root))
3945
3213
  ) {
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
- });
3214
+ const registrationRestored = await recoveryStep(
3215
+ `restore the removed ${transaction.host} registration`,
3216
+ async () => {
3217
+ addRegistration(
3218
+ transaction.host,
3219
+ transaction.paths.root,
3220
+ ["installed", "disabled"].includes(transaction.previousState.kind),
3221
+ { enabled: transaction.previousState.kind !== "disabled" },
3222
+ );
3223
+ },
3224
+ );
3954
3225
  complete = registrationRestored && complete;
3955
3226
  } else if (transaction.registrationRemoved && transaction.previousEntry !== null) {
3956
3227
  complete = false;
3957
3228
  }
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
- }
3229
+
3992
3230
  transaction.originalStateRestored = complete;
3993
3231
  return complete;
3994
3232
  }
@@ -3997,48 +3235,23 @@ async function commitRemoval(transaction, { beforeBackupDelete = null } = {}) {
3997
3235
  if (transaction.backupCreated && (await entryExists(transaction.backup))) {
3998
3236
  await verifyManagedTreeForPurge(transaction.host, transaction.backup);
3999
3237
  if (beforeBackupDelete !== null) {
4000
- await beforeBackupDelete(Object.freeze({
4001
- host: transaction.host,
4002
- backup: transaction.backup,
4003
- }));
3238
+ await beforeBackupDelete(
3239
+ Object.freeze({
3240
+ host: transaction.host,
3241
+ backup: transaction.backup,
3242
+ }),
3243
+ );
4004
3244
  }
4005
3245
  await rm(transaction.backup, { recursive: true });
4006
3246
  transaction.backupCreated = false;
4007
3247
  transaction.rootCommitted = true;
4008
3248
  }
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
3249
  }
4026
3250
 
4027
3251
  async function validateRemovalCommit(transaction) {
4028
3252
  if (transaction.backupCreated && (await entryExists(transaction.backup))) {
4029
3253
  await verifyManagedTreeForPurge(transaction.host, transaction.backup);
4030
3254
  }
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
3255
  }
4043
3256
 
4044
3257
  function xmlEscape(value) {
@@ -4057,7 +3270,9 @@ async function executablePath(name, environmentOverride) {
4057
3270
  await access(candidate, fsConstants.X_OK);
4058
3271
  return candidate;
4059
3272
  }
4060
- const which = run("/usr/bin/which", [name], { allowFailure: true });
3273
+ const which = run(process.platform === "win32" ? "where.exe" : "/usr/bin/which", [name], {
3274
+ allowFailure: true,
3275
+ });
4061
3276
  const candidate = which.status === 0 ? which.stdout.trim() : "";
4062
3277
  if (!candidate || !candidate.startsWith("/")) {
4063
3278
  fail(`could not find an executable ${name} for the automatic updater`);
@@ -4080,14 +3295,21 @@ function launchctlDomain() {
4080
3295
  function launchAgentDocument(npx, channel, environment) {
4081
3296
  const packageTag = channel === "next" ? "next" : "latest";
4082
3297
  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));
3298
+ const argumentsXml = arguments_
3299
+ .map((argument) => ` <string>${xmlEscape(argument)}</string>`)
3300
+ .join("\n");
3301
+ const environmentEntries = Object.entries(environment).sort(([left], [right]) =>
3302
+ left.localeCompare(right),
3303
+ );
4085
3304
  const environmentXml =
4086
3305
  environmentEntries.length === 0
4087
3306
  ? ""
4088
3307
  : `\n <key>EnvironmentVariables</key>\n <dict>\n` +
4089
3308
  environmentEntries
4090
- .map(([key, value]) => ` <key>${xmlEscape(key)}</key>\n` + ` <string>${xmlEscape(value)}</string>`)
3309
+ .map(
3310
+ ([key, value]) =>
3311
+ ` <key>${xmlEscape(key)}</key>\n` + ` <string>${xmlEscape(value)}</string>`,
3312
+ )
4091
3313
  .join("\n") +
4092
3314
  "\n </dict>";
4093
3315
  return `<?xml version="1.0" encoding="UTF-8"?>
@@ -4120,6 +3342,7 @@ function launchAgentTarget() {
4120
3342
  }
4121
3343
 
4122
3344
  function launchAgentLoaded() {
3345
+ if (process.platform === "win32") return false;
4123
3346
  return (
4124
3347
  run(launchctlBinary(), ["print", launchAgentTarget()], {
4125
3348
  allowFailure: true,
@@ -4141,69 +3364,19 @@ function stopLoadedLaunchAgent() {
4141
3364
  async function updaterEnvironment(hosts, npx) {
4142
3365
  const node = await executablePath("node", "OPENSOCRATES_NODE_BIN");
4143
3366
  const environment = {
4144
- PATH: [...new Set([dirname(npx), dirname(node), "/usr/bin", "/bin", "/usr/sbin", "/sbin"])].join(":"),
3367
+ PATH: [
3368
+ ...new Set([dirname(npx), dirname(node), "/usr/bin", "/bin", "/usr/sbin", "/sbin"]),
3369
+ ].join(":"),
4145
3370
  };
4146
3371
  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";
3372
+ const key = "CODEX_BIN";
4194
3373
  const executable = await executablePath(host, key);
4195
3374
  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
- ]) {
3375
+ environment.PATH = [...new Set([dirname(executable), ...environment.PATH.split(":")])].join(
3376
+ ":",
3377
+ );
3378
+ }
3379
+ for (const key of ["CODEX_HOME", "OPENSOCRATES_STATE_DIR"]) {
4207
3380
  if (process.env[key]) environment[key] = resolve(process.env[key]);
4208
3381
  }
4209
3382
  return environment;
@@ -4211,7 +3384,10 @@ async function updaterEnvironment(hosts, npx) {
4211
3384
 
4212
3385
  async function installLaunchAgent(channel, hosts) {
4213
3386
  if (process.platform !== "darwin" || process.arch !== "arm64") {
4214
- fail(`automatic updates currently support darwin-arm64 only; detected ` + `${process.platform}-${process.arch}`);
3387
+ fail(
3388
+ `automatic updates currently support darwin-arm64 only; detected ` +
3389
+ `${process.platform}-${process.arch}`,
3390
+ );
4215
3391
  }
4216
3392
  const paths = statePaths();
4217
3393
  const npx = await executablePath("npx", "OPENSOCRATES_NPX_BIN");
@@ -4232,16 +3408,22 @@ async function installLaunchAgent(channel, hosts) {
4232
3408
  const wasLoaded = launchAgentLoaded();
4233
3409
  if (wasLoaded) stopLoadedLaunchAgent();
4234
3410
  await atomicWritePrivateFile(paths.launchAgent, document);
4235
- const launched = run(launchctlBinary(), ["bootstrap", launchctlDomain(), paths.launchAgent], { allowFailure: true });
3411
+ const launched = run(launchctlBinary(), ["bootstrap", launchctlDomain(), paths.launchAgent], {
3412
+ allowFailure: true,
3413
+ });
4236
3414
  if (launched.status !== 0) {
4237
3415
  if (previousDocument === null) {
4238
3416
  await rm(paths.launchAgent, { force: true });
4239
3417
  } else {
4240
3418
  await atomicWritePrivateFile(paths.launchAgent, previousDocument);
4241
3419
  if (wasLoaded) {
4242
- const restored = run(launchctlBinary(), ["bootstrap", launchctlDomain(), paths.launchAgent], {
4243
- allowFailure: true,
4244
- });
3420
+ const restored = run(
3421
+ launchctlBinary(),
3422
+ ["bootstrap", launchctlDomain(), paths.launchAgent],
3423
+ {
3424
+ allowFailure: true,
3425
+ },
3426
+ );
4245
3427
  if (restored.status !== 0) {
4246
3428
  console.error("warning: the previous OpenSocrates LaunchAgent could not be reloaded");
4247
3429
  }
@@ -4267,7 +3449,9 @@ async function requireOwnedLaunchAgent(target) {
4267
3449
  ];
4268
3450
  const arguments_ =
4269
3451
  argumentBlocks.length === 1
4270
- ? [...argumentBlocks[0][1].matchAll(/<string>([\s\S]*?)<\/string>/gu)].map((match) => match[1].trim())
3452
+ ? [...argumentBlocks[0][1].matchAll(/<string>([\s\S]*?)<\/string>/gu)].map((match) =>
3453
+ match[1].trim(),
3454
+ )
4271
3455
  : [];
4272
3456
  if (
4273
3457
  [...document.matchAll(labelPattern)].length !== 1 ||
@@ -4286,7 +3470,11 @@ async function disableLaunchAgent() {
4286
3470
  const paths = statePaths();
4287
3471
  const agentPresent = await entryExists(paths.launchAgent);
4288
3472
  if (agentPresent) {
4289
- await requireSafePathBelow(paths.launchAgentsDirectory, paths.launchAgent, "OpenSocrates LaunchAgent");
3473
+ await requireSafePathBelow(
3474
+ paths.launchAgentsDirectory,
3475
+ paths.launchAgent,
3476
+ "OpenSocrates LaunchAgent",
3477
+ );
4290
3478
  await requireOwnedLaunchAgent(paths.launchAgent);
4291
3479
  }
4292
3480
  if (process.env.OPENSOCRATES_SKIP_LAUNCHCTL !== "1") {
@@ -4308,7 +3496,9 @@ async function runStandardRemove(options, { report = true, beforeBackupDelete =
4308
3496
  const preflights = await preflightSelectedHosts(options, "remove", desired);
4309
3497
  const removedHosts = preflights.map((item) => item.host);
4310
3498
  const remainingHosts = desired.installedHosts.filter((host) => !removedHosts.includes(host));
4311
- const remainingAutoUpdateHosts = desired.autoUpdate.hosts.filter((host) => !removedHosts.includes(host));
3499
+ const remainingAutoUpdateHosts = desired.autoUpdate.hosts.filter(
3500
+ (host) => !removedHosts.includes(host),
3501
+ );
4312
3502
  const keepAutoUpdate = desired.autoUpdate.enabled && remainingAutoUpdateHosts.length > 0;
4313
3503
  const nextDesired = {
4314
3504
  ...desired,
@@ -4358,11 +3548,16 @@ async function runStandardRemove(options, { report = true, beforeBackupDelete =
4358
3548
  rollbackComplete = restored && rollbackComplete;
4359
3549
  }
4360
3550
  const unrestoredHosts = new Set(
4361
- transactions.filter((transaction) => transaction.originalStateRestored !== true).map((item) => item.host),
3551
+ transactions
3552
+ .filter((transaction) => transaction.originalStateRestored !== true)
3553
+ .map((item) => item.host),
4362
3554
  );
4363
3555
  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;
3556
+ const recoveredAutoUpdateHosts = desired.autoUpdate.hosts.filter((host) =>
3557
+ recoveredHosts.includes(host),
3558
+ );
3559
+ const keepRecoveredAutoUpdate =
3560
+ desired.autoUpdate.enabled && recoveredAutoUpdateHosts.length > 0;
4366
3561
  let recoveredDesired = {
4367
3562
  ...desired,
4368
3563
  installedHosts: recoveredHosts,
@@ -4373,13 +3568,16 @@ async function runStandardRemove(options, { report = true, beforeBackupDelete =
4373
3568
  };
4374
3569
  let schedulerRestored = true;
4375
3570
  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
- });
3571
+ schedulerRestored = await recoveryStep(
3572
+ "reconcile the automatic updater after removal failure",
3573
+ async () => {
3574
+ if (recoveredDesired.autoUpdate.enabled) {
3575
+ await installLaunchAgent(recoveredDesired.channel, recoveredDesired.autoUpdate.hosts);
3576
+ } else {
3577
+ await disableLaunchAgent();
3578
+ }
3579
+ },
3580
+ );
4383
3581
  if (!schedulerRestored && recoveredDesired.autoUpdate.enabled) {
4384
3582
  recoveredDesired = {
4385
3583
  ...recoveredDesired,
@@ -4393,16 +3591,21 @@ async function runStandardRemove(options, { report = true, beforeBackupDelete =
4393
3591
  rollbackComplete = schedulerRestored && rollbackComplete;
4394
3592
  }
4395
3593
  if (desiredWritten || !rollbackComplete || !schedulerRestored) {
4396
- const stateRestored = await recoveryStep("record desired state after removal failure", async () => {
4397
- await writeDesiredState(recoveredDesired);
4398
- });
3594
+ const stateRestored = await recoveryStep(
3595
+ "record desired state after removal failure",
3596
+ async () => {
3597
+ await writeDesiredState(recoveredDesired);
3598
+ },
3599
+ );
4399
3600
  rollbackComplete = stateRestored && rollbackComplete;
4400
3601
  }
4401
3602
  if (!rollbackComplete) {
4402
3603
  console.error("error: removal cleanup is incomplete; no success was recorded.");
4403
3604
  for (const transaction of transactions) {
4404
3605
  if (!transaction.backupCreated || !(await entryExists(transaction.backup))) continue;
4405
- console.error(`error: preserved ${transaction.host} removal residue: ${transaction.backup}`);
3606
+ console.error(
3607
+ `error: preserved ${transaction.host} removal residue: ${transaction.backup}`,
3608
+ );
4406
3609
  console.error(
4407
3610
  `error: safe cleanup retry: opensocrates remove --host ${transaction.host} --purge`,
4408
3611
  );
@@ -4416,22 +3619,13 @@ async function runStandardRemove(options, { report = true, beforeBackupDelete =
4416
3619
  }
4417
3620
  if (!report) return { removedHosts, desired: nextDesired };
4418
3621
  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";
3622
+ const label = "Codex";
4431
3623
  console.log(`OpenSocrates was removed from ${label}.`);
4432
3624
  }
4433
3625
  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.");
3626
+ console.log(
3627
+ "Removal scope: registrations and installer-managed roots only; this is not a complete uninstall.",
3628
+ );
4435
3629
  const residue = [];
4436
3630
  for (const host of removedHosts) {
4437
3631
  const paths = purgePathsFor(host);
@@ -4446,14 +3640,13 @@ async function runStandardRemove(options, { report = true, beforeBackupDelete =
4446
3640
  if (removedHosts.includes("codex")) {
4447
3641
  console.log("Codex OpenSocrates hook trust is preserved by this removal contract.");
4448
3642
  }
4449
- console.log(`Next: close active hosts, then run opensocrates remove --host ${options.host} --purge.`);
3643
+ console.log(
3644
+ `Next: close active hosts, then run opensocrates remove --host ${options.host} --purge.`,
3645
+ );
4450
3646
  return { removedHosts, desired: nextDesired };
4451
3647
  }
4452
3648
 
4453
3649
  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
3650
  try {
4458
3651
  requireHostCli(host);
4459
3652
  } catch (error) {
@@ -4462,7 +3655,7 @@ async function purgeRegistration(host, paths) {
4462
3655
  detail: error instanceof Error ? error.message : String(error),
4463
3656
  };
4464
3657
  }
4465
- if (host === "claude") warnLegacyClaudeInstallation();
3658
+
4466
3659
  const entry = marketplaceEntry(host);
4467
3660
  if (entry !== null && entryRoot(entry, host) !== paths.root) {
4468
3661
  fail(`${host} OpenSocrates registration points to an unmanaged location`);
@@ -4497,7 +3690,9 @@ function purgeComponentBlocksCompletion(component) {
4497
3690
  }
4498
3691
 
4499
3692
  function finalizePurgeHostResult(hostResult) {
4500
- const registrationComplete = new Set(["removed", "absent", "not-applicable"]).has(hostResult.registration);
3693
+ const registrationComplete = new Set(["removed", "absent", "not-applicable"]).has(
3694
+ hostResult.registration,
3695
+ );
4501
3696
  hostResult.status =
4502
3697
  registrationComplete &&
4503
3698
  !hostResult.components.some(purgeComponentBlocksCompletion) &&
@@ -4510,7 +3705,9 @@ function finalizePurgeHostResult(hostResult) {
4510
3705
  async function resetPurgeTrustExtension(hostResult, options) {
4511
3706
  if (hostResult.host !== "codex" || !options.resetTrust) return true;
4512
3707
  try {
4513
- const reset = await resetCodexOpenSocratesHookTrust({ hooks: options.trustResetHooks ?? {} });
3708
+ const reset = await resetCodexOpenSocratesHookTrust({
3709
+ hooks: options.trustResetHooks ?? {},
3710
+ });
4514
3711
  hostResult.extension = {
4515
3712
  component: "host-security-trust",
4516
3713
  status: reset.status,
@@ -4539,9 +3736,7 @@ function preservePurgeComponentsAfterTrustFailure(hostResult, paths) {
4539
3736
  ["managed-root", paths.root],
4540
3737
  ["transaction-residue", null],
4541
3738
  ];
4542
- if (hostResult.host === "opencode") {
4543
- preserved.push(["opencode-bridge", paths.bridge], ["opencode-bridge-residue", null]);
4544
- }
3739
+
4545
3740
  if (paths.cacheRoot !== null) preserved.push(["plugin-cache", paths.cacheRoot]);
4546
3741
  preserved.push(...paths.pluginData.map((path) => ["plugin-data", path]));
4547
3742
  for (const [component, path] of preserved) {
@@ -4576,30 +3771,25 @@ async function purgeOneHost(hostResult, options) {
4576
3771
  await capturePurgeComponent(hostResult, "managed-root", () =>
4577
3772
  removeOwnedManagedRoot(hostResult.host, paths),
4578
3773
  );
4579
- if (hostResult.host === "opencode") {
4580
- await capturePurgeComponent(hostResult, "opencode-bridge", () => purgeOpenCodeBridge(paths));
4581
- }
3774
+
4582
3775
  await capturePurgeComponent(hostResult, "transaction-residue", () =>
4583
3776
  cleanupTransientRootResidue(hostResult.host, paths),
4584
3777
  );
4585
- if (hostResult.host === "opencode") {
4586
- await capturePurgeComponent(hostResult, "opencode-bridge-residue", () =>
4587
- purgeOpenCodeBridgeResidue(paths),
4588
- );
4589
- }
3778
+
4590
3779
  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));
3780
+ await capturePurgeComponent(hostResult, "plugin-cache", () =>
3781
+ purgeHostCache(hostResult.host, paths),
3782
+ );
4595
3783
  }
4596
3784
  finalizePurgeHostResult(hostResult);
4597
3785
  }
4598
3786
 
4599
3787
  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),
3788
+ const registrationComplete = new Set(["removed", "absent", "not-applicable"]).has(
3789
+ hostResult.registration,
3790
+ );
3791
+ const activationComponents = hostResult.components.filter(
3792
+ (item) => item.component === "managed-root",
4603
3793
  );
4604
3794
  return registrationComplete && !activationComponents.some(purgeComponentBlocksCompletion);
4605
3795
  }
@@ -4708,7 +3898,11 @@ async function preparePurgeStateFinalization() {
4708
3898
  const renamed = [];
4709
3899
  try {
4710
3900
  for (const item of tombstones) {
4711
- await requireSafePathBelow(paths.directory, item.tombstone, "OpenSocrates prior purge tombstone");
3901
+ await requireSafePathBelow(
3902
+ paths.directory,
3903
+ item.tombstone,
3904
+ "OpenSocrates prior purge tombstone",
3905
+ );
4712
3906
  const info = await lstat(item.tombstone);
4713
3907
  if (!info.isFile() || info.isSymbolicLink()) {
4714
3908
  fail(`refusing to remove an unsafe prior OpenSocrates purge tombstone: ${item.tombstone}`);
@@ -4721,7 +3915,10 @@ async function preparePurgeStateFinalization() {
4721
3915
  if (!info.isFile() || info.isSymbolicLink()) {
4722
3916
  fail(`refusing to remove an unsafe OpenSocrates state file: ${target}`);
4723
3917
  }
4724
- const tombstone = join(paths.directory, `.purge-finalize-${randomUUID()}-${basename(target)}`);
3918
+ const tombstone = join(
3919
+ paths.directory,
3920
+ `.purge-finalize-${randomUUID()}-${basename(target)}`,
3921
+ );
4725
3922
  await rename(target, tombstone);
4726
3923
  const item = { original: target, tombstone };
4727
3924
  tombstones.push(item);
@@ -4865,7 +4062,9 @@ function reportPurgeResult(result) {
4865
4062
  console.log("User task, project, chat, plan, and history data was preserved.");
4866
4063
  } else {
4867
4064
  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.");
4065
+ console.error(
4066
+ "Resolve the reported item, close active hosts if needed, and rerun the same purge command.",
4067
+ );
4869
4068
  }
4870
4069
  }
4871
4070
 
@@ -4917,14 +4116,19 @@ async function enableAutoUpdate(options) {
4917
4116
  const observedVersions = new Set();
4918
4117
  for (const host of installedHosts) {
4919
4118
  const preflight = preflights.get(host) ?? (await preflightHost(host, "update"));
4920
- if (preflight.previousState.kind !== "installed" || typeof preflight.previousState.version !== "string") {
4119
+ if (
4120
+ preflight.previousState.kind !== "installed" ||
4121
+ typeof preflight.previousState.version !== "string"
4122
+ ) {
4921
4123
  fail(`cannot determine the installed OpenSocrates version on ${host}`);
4922
4124
  }
4923
4125
  preflights.set(host, preflight);
4924
4126
  observedVersions.add(preflight.previousState.version);
4925
4127
  }
4926
4128
  if (observedVersions.size !== 1) {
4927
- fail("installed hosts do not share one known version; run update --host all before enabling automatic updates");
4129
+ fail(
4130
+ "installed hosts do not share one known version; run update --host all before enabling automatic updates",
4131
+ );
4928
4132
  }
4929
4133
  [activeVersion] = observedVersions;
4930
4134
  }
@@ -4990,7 +4194,9 @@ async function runScheduledUpdate(options) {
4990
4194
  }
4991
4195
  const scheduledAt = Date.parse(desired.autoUpdate.nextCheckAt ?? "");
4992
4196
  if (!options.force && Number.isFinite(scheduledAt) && scheduledAt > Date.now()) {
4993
- console.log(`The next automatic update check is scheduled for ${desired.autoUpdate.nextCheckAt}.`);
4197
+ console.log(
4198
+ `The next automatic update check is scheduled for ${desired.autoUpdate.nextCheckAt}.`,
4199
+ );
4994
4200
  return;
4995
4201
  }
4996
4202
  const checkedAt = nowIso();
@@ -5020,7 +4226,9 @@ async function runScheduledUpdate(options) {
5020
4226
  result: "blocked",
5021
4227
  errorCategory: "major-policy",
5022
4228
  });
5023
- console.log(`OpenSocrates ${PRODUCT_VERSION} is available but blocked by the major-version policy.`);
4229
+ console.log(
4230
+ `OpenSocrates ${PRODUCT_VERSION} is available but blocked by the major-version policy.`,
4231
+ );
5024
4232
  return;
5025
4233
  }
5026
4234
  try {
@@ -5028,7 +4236,10 @@ async function runScheduledUpdate(options) {
5028
4236
  for (const host of hosts) ready.push(await preflightHost(host, "update"));
5029
4237
  const alreadyCurrent =
5030
4238
  desired.activeVersion === PRODUCT_VERSION &&
5031
- ready.every((item) => item.previousState.kind === "installed" && item.previousState.version === PRODUCT_VERSION);
4239
+ ready.every(
4240
+ (item) =>
4241
+ item.previousState.kind === "installed" && item.previousState.version === PRODUCT_VERSION,
4242
+ );
5032
4243
  if (!alreadyCurrent) {
5033
4244
  if (hosts.length > 1) {
5034
4245
  await runInstallOrUpdate({ ...options, host: ALL_HOST }, "update");
@@ -5080,7 +4291,14 @@ async function runScheduledUpdate(options) {
5080
4291
  await writeAutoUpdateReceipt({
5081
4292
  version: PRODUCT_VERSION,
5082
4293
  checkedAt,
5083
- hosts: hosts.map((host) => ({ host, result: "failed" })),
4294
+ hosts:
4295
+ error instanceof PackagePreparationError
4296
+ ? error.hostOutcomes
4297
+ : hosts.map((host) => ({
4298
+ host,
4299
+ result: "failed",
4300
+ errorCategory: errorCategory(error),
4301
+ })),
5084
4302
  result: "failed",
5085
4303
  errorCategory: errorCategory(error),
5086
4304
  });
@@ -5090,9 +4308,14 @@ async function runScheduledUpdate(options) {
5090
4308
  }
5091
4309
 
5092
4310
  function requireSupportedPlatform() {
5093
- if (process.platform !== "darwin" || process.arch !== "arm64") {
4311
+ if (
4312
+ !(
4313
+ (process.platform === "darwin" && process.arch === "arm64") ||
4314
+ (process.platform === "win32" && process.arch === "x64")
4315
+ )
4316
+ ) {
5094
4317
  fail(
5095
- `OpenSocrates ${PRODUCT_VERSION} prebuilt installation supports darwin-arm64 only; ` +
4318
+ `OpenSocrates ${PRODUCT_VERSION} prebuilt installation supports darwin-arm64 and windows-x64; ` +
5096
4319
  `detected ${process.platform}-${process.arch}`,
5097
4320
  );
5098
4321
  }
@@ -5101,26 +4324,18 @@ function requireSupportedPlatform() {
5101
4324
  async function verifyPackages(options) {
5102
4325
  const assetHosts = qualifiedAssetHosts(options);
5103
4326
  const hosts =
5104
- options.host === ALL_HOST ? (assetHosts.length > 0 ? assetHosts : SUPPORTED_HOSTS) : [options.host];
4327
+ options.host === ALL_HOST
4328
+ ? assetHosts.length > 0
4329
+ ? assetHosts
4330
+ : SUPPORTED_HOSTS
4331
+ : [options.host];
5105
4332
  const prepared = await prepareVerifiedPackages(options, hosts);
5106
4333
  try {
5107
4334
  for (const item of prepared) {
5108
4335
  console.log(
5109
- `${item.host}: verified OpenSocrates ${PRODUCT_VERSION} release and ` + `${item.checkedFiles} package files.`,
4336
+ `${item.host}: verified OpenSocrates ${PRODUCT_VERSION} release and ` +
4337
+ `${item.checkedFiles} package files.`,
5110
4338
  );
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
4339
  }
5125
4340
  } finally {
5126
4341
  await Promise.all(prepared.map((item) => rm(item.scratch, { recursive: true, force: true })));
@@ -5129,6 +4344,15 @@ async function verifyPackages(options) {
5129
4344
 
5130
4345
  export async function main(argv = process.argv.slice(2), internalDependencies = {}) {
5131
4346
  const options = parseCli(argv);
4347
+ if (process.platform === "win32" && options.action === "auto-update") {
4348
+ const message =
4349
+ "Automatic updates: unavailable on Windows in 1.4.0. Use opensocrates update --host all manually.";
4350
+ if (options.autoUpdateAction === "status" || options.autoUpdateAction === "disable") {
4351
+ console.log(message);
4352
+ return 0;
4353
+ }
4354
+ fail(message);
4355
+ }
5132
4356
  if (options.action === "help") {
5133
4357
  showHelp();
5134
4358
  return 0;