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