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