opensocrates 1.1.4 → 1.2.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 +84 -0
- package/README.ko.md +84 -34
- package/README.md +105 -37
- package/SECURITY.md +7 -0
- package/VERSION +1 -1
- package/installer/opensocrates.mjs +938 -276
- package/package.json +8 -3
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { createHash, randomInt, randomUUID } from "node:crypto";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
constants as fsConstants,
|
|
7
|
+
createReadStream,
|
|
8
|
+
createWriteStream,
|
|
9
|
+
existsSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
realpathSync,
|
|
12
|
+
} from "node:fs";
|
|
6
13
|
import {
|
|
7
14
|
access,
|
|
8
15
|
chmod,
|
|
@@ -24,13 +31,20 @@ import { Readable } from "node:stream";
|
|
|
24
31
|
import { pipeline } from "node:stream/promises";
|
|
25
32
|
import { fileURLToPath } from "node:url";
|
|
26
33
|
|
|
27
|
-
export const PRODUCT_VERSION = "1.
|
|
34
|
+
export const PRODUCT_VERSION = "1.2.0";
|
|
28
35
|
export const REPOSITORY = "ParkerHwang/OpenSocrates";
|
|
29
36
|
export const MARKETPLACE_NAME = "opensocrates";
|
|
30
37
|
export const PLUGIN_NAME = "opensocrates";
|
|
31
38
|
export const PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
|
|
32
39
|
export const DEFAULT_HOST = "codex";
|
|
33
|
-
export const SUPPORTED_HOSTS = Object.freeze([
|
|
40
|
+
export const SUPPORTED_HOSTS = Object.freeze([
|
|
41
|
+
"antigravity",
|
|
42
|
+
"claude",
|
|
43
|
+
"codex",
|
|
44
|
+
"cursor",
|
|
45
|
+
"grok",
|
|
46
|
+
"opencode",
|
|
47
|
+
]);
|
|
34
48
|
export const ALL_HOST = "all";
|
|
35
49
|
export const HOST_CHOICES = Object.freeze([ALL_HOST, ...SUPPORTED_HOSTS]);
|
|
36
50
|
export function assetNameFor(host = DEFAULT_HOST) {
|
|
@@ -54,16 +68,75 @@ const CLAUDE_MARKER = Object.freeze({
|
|
|
54
68
|
pluginName: PLUGIN_NAME,
|
|
55
69
|
host: "claude",
|
|
56
70
|
});
|
|
71
|
+
const CURSOR_MARKER = Object.freeze({
|
|
72
|
+
schemaVersion: 1,
|
|
73
|
+
marketplaceName: MARKETPLACE_NAME,
|
|
74
|
+
pluginName: PLUGIN_NAME,
|
|
75
|
+
host: "cursor",
|
|
76
|
+
registrationKind: "file-drop",
|
|
77
|
+
});
|
|
78
|
+
const ANTIGRAVITY_MARKER = Object.freeze({
|
|
79
|
+
schemaVersion: 1,
|
|
80
|
+
marketplaceName: MARKETPLACE_NAME,
|
|
81
|
+
pluginName: PLUGIN_NAME,
|
|
82
|
+
host: "antigravity",
|
|
83
|
+
registrationKind: "file-drop",
|
|
84
|
+
});
|
|
85
|
+
const GROK_MARKER = Object.freeze({
|
|
86
|
+
schemaVersion: 1,
|
|
87
|
+
marketplaceName: MARKETPLACE_NAME,
|
|
88
|
+
pluginName: PLUGIN_NAME,
|
|
89
|
+
host: "grok",
|
|
90
|
+
registrationKind: "file-drop",
|
|
91
|
+
});
|
|
92
|
+
const OPENCODE_MARKER = Object.freeze({
|
|
93
|
+
schemaVersion: 1,
|
|
94
|
+
marketplaceName: MARKETPLACE_NAME,
|
|
95
|
+
pluginName: PLUGIN_NAME,
|
|
96
|
+
host: "opencode",
|
|
97
|
+
registrationKind: "automatic-file-discovery",
|
|
98
|
+
});
|
|
99
|
+
const OPENCODE_INSTALL_MANIFEST = ".opensocrates-installation.json";
|
|
100
|
+
const OPENCODE_BRIDGE_MARKER = ".opensocrates-managed.json";
|
|
101
|
+
// OpenCode is discovered automatically from its config directory, so it is
|
|
102
|
+
// deliberately not a file-drop host.
|
|
103
|
+
const FILE_DROP_HOSTS = Object.freeze(["antigravity", "cursor", "grok"]);
|
|
57
104
|
const HOST_LAYOUTS = Object.freeze({
|
|
105
|
+
antigravity: {
|
|
106
|
+
marketplaceRelative: null,
|
|
107
|
+
pluginRelative: ".",
|
|
108
|
+
manifestRelative: "plugin.json",
|
|
109
|
+
requiresRuntime: false,
|
|
110
|
+
},
|
|
58
111
|
claude: {
|
|
59
112
|
marketplaceRelative: join(".claude-plugin", "marketplace.json"),
|
|
60
113
|
pluginRelative: join("plugins", PLUGIN_NAME),
|
|
61
114
|
manifestRelative: join(".claude-plugin", "plugin.json"),
|
|
115
|
+
requiresRuntime: true,
|
|
62
116
|
},
|
|
63
117
|
codex: {
|
|
64
118
|
marketplaceRelative: join(".agents", "plugins", "marketplace.json"),
|
|
65
119
|
pluginRelative: join("build", "generated", "plugins", "codex"),
|
|
66
120
|
manifestRelative: join(".codex-plugin", "plugin.json"),
|
|
121
|
+
requiresRuntime: true,
|
|
122
|
+
},
|
|
123
|
+
cursor: {
|
|
124
|
+
marketplaceRelative: null,
|
|
125
|
+
pluginRelative: ".",
|
|
126
|
+
manifestRelative: "plugin.json",
|
|
127
|
+
requiresRuntime: false,
|
|
128
|
+
},
|
|
129
|
+
grok: {
|
|
130
|
+
marketplaceRelative: null,
|
|
131
|
+
pluginRelative: ".",
|
|
132
|
+
manifestRelative: "plugin.json",
|
|
133
|
+
requiresRuntime: false,
|
|
134
|
+
},
|
|
135
|
+
opencode: {
|
|
136
|
+
marketplaceRelative: null,
|
|
137
|
+
pluginRelative: ".",
|
|
138
|
+
manifestRelative: "opencode-plugin.json",
|
|
139
|
+
requiresRuntime: false,
|
|
67
140
|
},
|
|
68
141
|
});
|
|
69
142
|
const MAX_DOWNLOAD_BYTES = 2 * 1024 * 1024 * 1024;
|
|
@@ -97,9 +170,7 @@ async function exists(target) {
|
|
|
97
170
|
}
|
|
98
171
|
|
|
99
172
|
export function statePaths() {
|
|
100
|
-
const stateDirectory = resolve(
|
|
101
|
-
process.env.OPENSOCRATES_STATE_DIR || join(homedir(), ".opensocrates"),
|
|
102
|
-
);
|
|
173
|
+
const stateDirectory = resolve(process.env.OPENSOCRATES_STATE_DIR || join(homedir(), ".opensocrates"));
|
|
103
174
|
const launchAgentsDirectory = resolve(
|
|
104
175
|
process.env.OPENSOCRATES_LAUNCH_AGENTS_DIR || join(homedir(), "Library", "LaunchAgents"),
|
|
105
176
|
);
|
|
@@ -135,12 +206,7 @@ function defaultDesiredState() {
|
|
|
135
206
|
}
|
|
136
207
|
|
|
137
208
|
function normalizeDesiredState(value) {
|
|
138
|
-
if (
|
|
139
|
-
value === null ||
|
|
140
|
-
typeof value !== "object" ||
|
|
141
|
-
Array.isArray(value) ||
|
|
142
|
-
value.schema !== DESIRED_STATE_SCHEMA
|
|
143
|
-
) {
|
|
209
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || value.schema !== DESIRED_STATE_SCHEMA) {
|
|
144
210
|
fail("the OpenSocrates desired-state manifest has an unsupported schema");
|
|
145
211
|
}
|
|
146
212
|
const installedHosts = value.installedHosts;
|
|
@@ -166,16 +232,10 @@ function normalizeDesiredState(value) {
|
|
|
166
232
|
fail("the OpenSocrates desired-state manifest has an invalid update policy");
|
|
167
233
|
}
|
|
168
234
|
const autoUpdateHosts =
|
|
169
|
-
value.autoUpdate.hosts === undefined
|
|
170
|
-
? value.autoUpdate.enabled
|
|
171
|
-
? installedHosts
|
|
172
|
-
: []
|
|
173
|
-
: value.autoUpdate.hosts;
|
|
235
|
+
value.autoUpdate.hosts === undefined ? (value.autoUpdate.enabled ? installedHosts : []) : value.autoUpdate.hosts;
|
|
174
236
|
if (
|
|
175
237
|
!Array.isArray(autoUpdateHosts) ||
|
|
176
|
-
autoUpdateHosts.some(
|
|
177
|
-
(host) => !SUPPORTED_HOSTS.includes(host) || !installedHosts.includes(host),
|
|
178
|
-
) ||
|
|
238
|
+
autoUpdateHosts.some((host) => !SUPPORTED_HOSTS.includes(host) || !installedHosts.includes(host)) ||
|
|
179
239
|
new Set(autoUpdateHosts).size !== autoUpdateHosts.length ||
|
|
180
240
|
(value.autoUpdate.enabled && autoUpdateHosts.length === 0) ||
|
|
181
241
|
(!value.autoUpdate.enabled && autoUpdateHosts.length > 0)
|
|
@@ -234,7 +294,11 @@ async function atomicWritePrivateFile(target, contents) {
|
|
|
234
294
|
await ensurePrivateDirectory(parent);
|
|
235
295
|
const temporary = join(parent, `.${basename(target)}.${randomUUID()}.tmp`);
|
|
236
296
|
try {
|
|
237
|
-
await writeFile(temporary, contents, {
|
|
297
|
+
await writeFile(temporary, contents, {
|
|
298
|
+
encoding: "utf8",
|
|
299
|
+
mode: 0o600,
|
|
300
|
+
flag: "wx",
|
|
301
|
+
});
|
|
238
302
|
await chmod(temporary, 0o600);
|
|
239
303
|
await rename(temporary, target);
|
|
240
304
|
} finally {
|
|
@@ -246,10 +310,7 @@ async function atomicWritePrivateFile(target, contents) {
|
|
|
246
310
|
|
|
247
311
|
export async function writeDesiredState(value) {
|
|
248
312
|
const normalized = normalizeDesiredState(value);
|
|
249
|
-
await atomicWritePrivateFile(
|
|
250
|
-
statePaths().desiredState,
|
|
251
|
-
`${JSON.stringify(normalized, null, 2)}\n`,
|
|
252
|
-
);
|
|
313
|
+
await atomicWritePrivateFile(statePaths().desiredState, `${JSON.stringify(normalized, null, 2)}\n`);
|
|
253
314
|
return normalized;
|
|
254
315
|
}
|
|
255
316
|
|
|
@@ -260,14 +321,14 @@ async function writeAutoUpdateReceipt({ version, checkedAt, hosts, result, error
|
|
|
260
321
|
checkedAt,
|
|
261
322
|
hosts: [...hosts]
|
|
262
323
|
.sort((left, right) => left.host.localeCompare(right.host))
|
|
263
|
-
.map(({ host, result: hostResult }) => ({
|
|
324
|
+
.map(({ host, result: hostResult }) => ({
|
|
325
|
+
host,
|
|
326
|
+
result: hostResult,
|
|
327
|
+
})),
|
|
264
328
|
result,
|
|
265
329
|
errorCategory: errorCategory ?? null,
|
|
266
330
|
};
|
|
267
|
-
await atomicWritePrivateFile(
|
|
268
|
-
statePaths().receipt,
|
|
269
|
-
`${JSON.stringify(receipt, null, 2)}\n`,
|
|
270
|
-
);
|
|
331
|
+
await atomicWritePrivateFile(statePaths().receipt, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
271
332
|
}
|
|
272
333
|
|
|
273
334
|
function nowIso() {
|
|
@@ -303,10 +364,7 @@ async function acquireOperationLock() {
|
|
|
303
364
|
try {
|
|
304
365
|
const handle = await open(paths.lock, "wx", 0o600);
|
|
305
366
|
try {
|
|
306
|
-
await handle.writeFile(
|
|
307
|
-
`${JSON.stringify({ pid: process.pid, startedAt: nowIso() })}\n`,
|
|
308
|
-
"utf8",
|
|
309
|
-
);
|
|
367
|
+
await handle.writeFile(`${JSON.stringify({ pid: process.pid, startedAt: nowIso() })}\n`, "utf8");
|
|
310
368
|
} catch (writeError) {
|
|
311
369
|
await handle.close().catch(() => undefined);
|
|
312
370
|
await rm(paths.lock, { force: true }).catch(() => undefined);
|
|
@@ -338,9 +396,7 @@ async function acquireOperationLock() {
|
|
|
338
396
|
}
|
|
339
397
|
stale =
|
|
340
398
|
ownerAlive === false ||
|
|
341
|
-
(ownerAlive === null &&
|
|
342
|
-
Number.isFinite(started) &&
|
|
343
|
-
Date.now() - started > LOCK_STALE_MILLISECONDS);
|
|
399
|
+
(ownerAlive === null && Number.isFinite(started) && Date.now() - started > LOCK_STALE_MILLISECONDS);
|
|
344
400
|
} catch {
|
|
345
401
|
const info = await lstat(paths.lock);
|
|
346
402
|
stale = Date.now() - info.mtimeMs > LOCK_STALE_MILLISECONDS;
|
|
@@ -373,16 +429,15 @@ function jsonEqual(left, right) {
|
|
|
373
429
|
}
|
|
374
430
|
|
|
375
431
|
function markerFor(host) {
|
|
432
|
+
if (host === "cursor") return CURSOR_MARKER;
|
|
433
|
+
if (host === "antigravity") return ANTIGRAVITY_MARKER;
|
|
434
|
+
if (host === "grok") return GROK_MARKER;
|
|
435
|
+
if (host === "opencode") return OPENCODE_MARKER;
|
|
376
436
|
return host === "claude" ? CLAUDE_MARKER : CODEX_MARKER;
|
|
377
437
|
}
|
|
378
438
|
|
|
379
439
|
export function markerMatches(value, host = DEFAULT_HOST) {
|
|
380
|
-
return (
|
|
381
|
-
value !== null &&
|
|
382
|
-
typeof value === "object" &&
|
|
383
|
-
!Array.isArray(value) &&
|
|
384
|
-
jsonEqual(value, markerFor(host))
|
|
385
|
-
);
|
|
440
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && jsonEqual(value, markerFor(host));
|
|
386
441
|
}
|
|
387
442
|
|
|
388
443
|
export function isSafeArchivePath(value) {
|
|
@@ -422,15 +477,7 @@ export function parseChecksumText(text, expectedName = ASSET_NAME) {
|
|
|
422
477
|
}
|
|
423
478
|
|
|
424
479
|
export function parseCli(argv) {
|
|
425
|
-
const actions = new Set([
|
|
426
|
-
"install",
|
|
427
|
-
"status",
|
|
428
|
-
"update",
|
|
429
|
-
"remove",
|
|
430
|
-
"verify",
|
|
431
|
-
"auto-update",
|
|
432
|
-
"help",
|
|
433
|
-
]);
|
|
480
|
+
const actions = new Set(["install", "status", "update", "remove", "verify", "auto-update", "help"]);
|
|
434
481
|
const args = [...argv];
|
|
435
482
|
let action = "install";
|
|
436
483
|
if (args[0] && !args[0].startsWith("-")) {
|
|
@@ -457,10 +504,7 @@ export function parseCli(argv) {
|
|
|
457
504
|
host: action === "auto-update" ? ALL_HOST : DEFAULT_HOST,
|
|
458
505
|
asset: null,
|
|
459
506
|
checksum: null,
|
|
460
|
-
hostAssets: {
|
|
461
|
-
claude: { asset: null, checksum: null },
|
|
462
|
-
codex: { asset: null, checksum: null },
|
|
463
|
-
},
|
|
507
|
+
hostAssets: Object.fromEntries(SUPPORTED_HOSTS.map((host) => [host, { asset: null, checksum: null }])),
|
|
464
508
|
channel: "stable",
|
|
465
509
|
intervalHours: AUTO_UPDATE_DEFAULT_INTERVAL_HOURS,
|
|
466
510
|
allowMajor: false,
|
|
@@ -518,7 +562,7 @@ export function parseCli(argv) {
|
|
|
518
562
|
options.intervalHours = value;
|
|
519
563
|
continue;
|
|
520
564
|
}
|
|
521
|
-
const qualified = flag?.match(
|
|
565
|
+
const qualified = flag?.match(new RegExp(`^--(asset|checksum)-(${SUPPORTED_HOSTS.join("|")})$`, "u"));
|
|
522
566
|
if (qualified) {
|
|
523
567
|
seenOptions.add(qualified[1]);
|
|
524
568
|
const value = args.shift();
|
|
@@ -550,17 +594,7 @@ export function parseCli(argv) {
|
|
|
550
594
|
if (options.host === ALL_HOST && options.asset !== null) {
|
|
551
595
|
fail("--host all requires host-qualified --asset-<host> and --checksum-<host> options");
|
|
552
596
|
}
|
|
553
|
-
if (
|
|
554
|
-
options.host === ALL_HOST &&
|
|
555
|
-
SUPPORTED_HOSTS.some((host) => options.hostAssets[host].asset !== null) &&
|
|
556
|
-
SUPPORTED_HOSTS.some((host) => options.hostAssets[host].asset === null)
|
|
557
|
-
) {
|
|
558
|
-
fail("local --host all verification requires asset and checksum pairs for both hosts");
|
|
559
|
-
}
|
|
560
|
-
if (
|
|
561
|
-
options.host !== ALL_HOST &&
|
|
562
|
-
SUPPORTED_HOSTS.some((host) => options.hostAssets[host].asset !== null)
|
|
563
|
-
) {
|
|
597
|
+
if (options.host !== ALL_HOST && SUPPORTED_HOSTS.some((host) => options.hostAssets[host].asset !== null)) {
|
|
564
598
|
fail("host-qualified asset options are only valid with --host all");
|
|
565
599
|
}
|
|
566
600
|
if (["status", "remove", "help"].includes(options.action) && options.asset !== null) {
|
|
@@ -575,13 +609,11 @@ export function parseCli(argv) {
|
|
|
575
609
|
if (
|
|
576
610
|
options.action === "auto-update" &&
|
|
577
611
|
options.autoUpdateAction !== "run" &&
|
|
578
|
-
(options.asset !== null ||
|
|
579
|
-
SUPPORTED_HOSTS.some((host) => options.hostAssets[host].asset !== null))
|
|
612
|
+
(options.asset !== null || SUPPORTED_HOSTS.some((host) => options.hostAssets[host].asset !== null))
|
|
580
613
|
) {
|
|
581
614
|
fail(`auto-update ${options.autoUpdateAction} does not accept local asset options`);
|
|
582
615
|
}
|
|
583
|
-
const autoUpdateEnable =
|
|
584
|
-
options.action === "auto-update" && options.autoUpdateAction === "enable";
|
|
616
|
+
const autoUpdateEnable = options.action === "auto-update" && options.autoUpdateAction === "enable";
|
|
585
617
|
const autoUpdateRun = options.action === "auto-update" && options.autoUpdateAction === "run";
|
|
586
618
|
for (const policyOption of ["allow-major", "channel", "interval-hours"]) {
|
|
587
619
|
if (seenOptions.has(policyOption) && !autoUpdateEnable) {
|
|
@@ -608,12 +640,12 @@ function showHelp() {
|
|
|
608
640
|
console.log(`OpenSocrates ${PRODUCT_VERSION}
|
|
609
641
|
|
|
610
642
|
Usage:
|
|
611
|
-
opensocrates install [--host all|codex|
|
|
612
|
-
opensocrates status [--host all|codex|
|
|
613
|
-
opensocrates update [--host all|codex|
|
|
614
|
-
opensocrates remove [--host all|codex|
|
|
615
|
-
opensocrates verify [--host all|codex|
|
|
616
|
-
opensocrates auto-update enable [--host all|codex|
|
|
643
|
+
opensocrates install [--host all|antigravity|claude|codex|cursor|grok|opencode] [--asset ZIP --checksum SHA256]
|
|
644
|
+
opensocrates status [--host all|antigravity|claude|codex|cursor|grok|opencode]
|
|
645
|
+
opensocrates update [--host all|antigravity|claude|codex|cursor|grok|opencode] [--asset ZIP --checksum SHA256]
|
|
646
|
+
opensocrates remove [--host all|antigravity|claude|codex|cursor|grok|opencode]
|
|
647
|
+
opensocrates verify [--host all|antigravity|claude|codex|cursor|grok|opencode] [--asset ZIP --checksum SHA256]
|
|
648
|
+
opensocrates auto-update enable [--host all|antigravity|claude|codex|cursor|grok|opencode]
|
|
617
649
|
[--channel stable|next] [--interval-hours ${AUTO_UPDATE_DEFAULT_INTERVAL_HOURS}]
|
|
618
650
|
[--allow-major]
|
|
619
651
|
opensocrates auto-update status
|
|
@@ -621,17 +653,36 @@ Usage:
|
|
|
621
653
|
|
|
622
654
|
Without --asset, install, update, and verify download the v${PRODUCT_VERSION}
|
|
623
655
|
package and checksum from GitHub Releases. The default lifecycle host is codex.
|
|
624
|
-
|
|
625
|
-
and
|
|
656
|
+
With --host all, supplied host-qualified asset/checksum pairs define the exact
|
|
657
|
+
transaction set and are never mixed with downloads for other hosts. Without
|
|
658
|
+
qualified assets, every ready host participates. Automatic updates are opt-in.
|
|
626
659
|
`);
|
|
627
660
|
}
|
|
628
661
|
|
|
629
662
|
function managedPaths(host) {
|
|
630
663
|
const configured =
|
|
631
|
-
host === "
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
664
|
+
host === "antigravity"
|
|
665
|
+
? process.env.ANTIGRAVITY_CONFIG_DIR
|
|
666
|
+
: host === "cursor"
|
|
667
|
+
? process.env.CURSOR_CONFIG_DIR
|
|
668
|
+
: host === "grok"
|
|
669
|
+
? process.env.GROK_HOME
|
|
670
|
+
: host === "opencode"
|
|
671
|
+
? process.env.OPENCODE_CONFIG_DIR
|
|
672
|
+
: host === "claude"
|
|
673
|
+
? process.env.CLAUDE_CONFIG_DIR
|
|
674
|
+
: process.env.CODEX_HOME;
|
|
675
|
+
const defaultHome =
|
|
676
|
+
host === "antigravity"
|
|
677
|
+
? join(homedir(), ".gemini", "config")
|
|
678
|
+
: host === "cursor"
|
|
679
|
+
? join(homedir(), ".cursor")
|
|
680
|
+
: host === "grok"
|
|
681
|
+
? join(homedir(), ".grok")
|
|
682
|
+
: host === "opencode"
|
|
683
|
+
? join(homedir(), ".config", "opencode")
|
|
684
|
+
: join(homedir(), host === "claude" ? ".claude" : ".codex");
|
|
685
|
+
const configuredHome = resolve(configured ? configured : defaultHome);
|
|
635
686
|
let hostHome = configuredHome;
|
|
636
687
|
try {
|
|
637
688
|
hostHome = realpathSync(configuredHome);
|
|
@@ -640,18 +691,46 @@ function managedPaths(host) {
|
|
|
640
691
|
// explicit, non-recursive target. Lexical resolution is the only
|
|
641
692
|
// available normalization until then.
|
|
642
693
|
}
|
|
643
|
-
const root =
|
|
694
|
+
const root =
|
|
695
|
+
host === "antigravity"
|
|
696
|
+
? join(hostHome, "plugins", PLUGIN_NAME)
|
|
697
|
+
: host === "cursor"
|
|
698
|
+
? join(hostHome, "plugins", "local", PLUGIN_NAME)
|
|
699
|
+
: host === "grok"
|
|
700
|
+
? join(hostHome, "plugins", PLUGIN_NAME)
|
|
701
|
+
: host === "opencode"
|
|
702
|
+
? join(hostHome, "skills", PLUGIN_NAME)
|
|
703
|
+
: join(hostHome, "managed-marketplaces", MARKETPLACE_NAME);
|
|
644
704
|
const layout = HOST_LAYOUTS[host];
|
|
645
705
|
return {
|
|
646
706
|
hostHome,
|
|
647
707
|
root,
|
|
648
708
|
parent: dirname(root),
|
|
649
709
|
marker: join(root, MARKER_NAME),
|
|
650
|
-
marketplace: join(root, layout.marketplaceRelative),
|
|
710
|
+
marketplace: typeof layout.marketplaceRelative === "string" ? join(root, layout.marketplaceRelative) : null,
|
|
651
711
|
plugin: join(root, layout.pluginRelative),
|
|
712
|
+
bridge: host === "opencode" ? join(hostHome, "plugins", "opensocrates.js") : null,
|
|
713
|
+
bridgeMarker: host === "opencode" ? join(hostHome, "plugins", OPENCODE_BRIDGE_MARKER) : null,
|
|
714
|
+
bridgeParent: host === "opencode" ? join(hostHome, "plugins") : null,
|
|
652
715
|
};
|
|
653
716
|
}
|
|
654
717
|
|
|
718
|
+
// Directory that holds staging trees and rollback backups for one host.
|
|
719
|
+
//
|
|
720
|
+
// Grok scans every directory below ~/.grok/plugins, including dot-prefixed
|
|
721
|
+
// ones, so a staging tree or rollback backup left there would be discovered as
|
|
722
|
+
// a second OpenSocrates plugin. An interrupted install must not be able to
|
|
723
|
+
// leave a shadow copy inside Grok's plugin discovery root, so both transient
|
|
724
|
+
// directories live in the Grok home instead, next to the scanned directory.
|
|
725
|
+
function transientParent(host, paths) {
|
|
726
|
+
return host === "grok" ? paths.hostHome : paths.parent;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export function transientPathsFor(host) {
|
|
730
|
+
const paths = managedPaths(host);
|
|
731
|
+
return { root: paths.root, parent: paths.parent, transient: transientParent(host, paths) };
|
|
732
|
+
}
|
|
733
|
+
|
|
655
734
|
function codexBinary() {
|
|
656
735
|
return process.env.CODEX_BIN || "codex";
|
|
657
736
|
}
|
|
@@ -660,6 +739,26 @@ function claudeBinary() {
|
|
|
660
739
|
return process.env.CLAUDE_BIN || "claude";
|
|
661
740
|
}
|
|
662
741
|
|
|
742
|
+
function cursorBinary() {
|
|
743
|
+
return process.env.CURSOR_BIN || "cursor";
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function cursorAppPaths() {
|
|
747
|
+
return ["/Applications/Cursor.app", join(homedir(), "Applications", "Cursor.app")];
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function antigravityBinary() {
|
|
751
|
+
return process.env.AGY_BIN || "agy";
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function grokBinary() {
|
|
755
|
+
return process.env.GROK_BIN || "grok";
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function opencodeBinary() {
|
|
759
|
+
return process.env.OPENCODE_BIN || "opencode";
|
|
760
|
+
}
|
|
761
|
+
|
|
663
762
|
function run(command, args, { allowFailure = false } = {}) {
|
|
664
763
|
const result = spawnSync(command, args, {
|
|
665
764
|
encoding: "utf8",
|
|
@@ -707,6 +806,10 @@ function runClaudeJson(args) {
|
|
|
707
806
|
return runJson(claudeBinary(), args, "Claude Code");
|
|
708
807
|
}
|
|
709
808
|
|
|
809
|
+
function runGrokJson(args) {
|
|
810
|
+
return runJson(grokBinary(), args, "Grok Build");
|
|
811
|
+
}
|
|
812
|
+
|
|
710
813
|
function claudeListEntries(payload, wrapper, label) {
|
|
711
814
|
if (Array.isArray(payload)) {
|
|
712
815
|
return payload;
|
|
@@ -752,11 +855,7 @@ function claudeMarketplaceEntries(payload) {
|
|
|
752
855
|
}
|
|
753
856
|
|
|
754
857
|
function claudePluginEntries(payload) {
|
|
755
|
-
return requireUniqueClaudeEntries(
|
|
756
|
-
claudeListEntries(payload, "plugins", "plugin list"),
|
|
757
|
-
"id",
|
|
758
|
-
"plugin list",
|
|
759
|
-
);
|
|
858
|
+
return requireUniqueClaudeEntries(claudeListEntries(payload, "plugins", "plugin list"), "id", "plugin list");
|
|
760
859
|
}
|
|
761
860
|
|
|
762
861
|
function versionAtLeast(value, minimum) {
|
|
@@ -773,12 +872,63 @@ function versionAtLeast(value, minimum) {
|
|
|
773
872
|
}
|
|
774
873
|
|
|
775
874
|
function requireHostCli(host, { authenticated = false } = {}) {
|
|
875
|
+
if (host === "grok") {
|
|
876
|
+
const result = run(grokBinary(), ["--version"]);
|
|
877
|
+
if (!versionAtLeast(result.stdout, [1, 0, 3])) {
|
|
878
|
+
fail(`Grok Build 1.0.3 or later is required; got ${result.stdout.trim()}`);
|
|
879
|
+
}
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
if (host === "cursor") {
|
|
883
|
+
if (process.env.CURSOR_CONFIG_DIR) return;
|
|
884
|
+
if (process.env.CURSOR_BIN) {
|
|
885
|
+
const result = run(cursorBinary(), ["--version"]);
|
|
886
|
+
if (!versionAtLeast(result.stdout, [2, 5, 0])) {
|
|
887
|
+
fail(`Cursor 2.5.0 or later is required; got ${result.stdout.trim()}`);
|
|
888
|
+
}
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
const app = cursorAppPaths().find((candidate) => existsSync(candidate));
|
|
892
|
+
if (app) {
|
|
893
|
+
const result = run(
|
|
894
|
+
"/usr/bin/plutil",
|
|
895
|
+
["-extract", "CFBundleShortVersionString", "raw", join(app, "Contents", "Info.plist")],
|
|
896
|
+
{ allowFailure: true },
|
|
897
|
+
);
|
|
898
|
+
if (result.status !== 0 || !versionAtLeast(result.stdout, [2, 5, 0])) {
|
|
899
|
+
fail("Cursor 2.5.0 or later is required for Agent Plugin support");
|
|
900
|
+
}
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
const result = run(cursorBinary(), ["--version"]);
|
|
904
|
+
if (!versionAtLeast(result.stdout, [2, 5, 0])) {
|
|
905
|
+
fail(`Cursor 2.5.0 or later is required; got ${result.stdout.trim()}`);
|
|
906
|
+
}
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
if (host === "antigravity") {
|
|
910
|
+
if (process.env.ANTIGRAVITY_CONFIG_DIR) return;
|
|
911
|
+
run(antigravityBinary(), ["--version"]);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
if (host === "opencode") {
|
|
915
|
+
// OPENCODE_CONFIG_DIR only redirects where the managed files are written;
|
|
916
|
+
// it does not waive the supported-host gate. Unlike the content-only
|
|
917
|
+
// file-drop hosts above, OpenCode receives an executing bridge bound to the
|
|
918
|
+
// stable chat.message contract, and the shipped package declares
|
|
919
|
+
// minimum_opencode_version 1.18.18 with a fail-closed <2.0.0 ceiling.
|
|
920
|
+
// Installing that bridge without confirming the host version would ship
|
|
921
|
+
// executable code against an unverified plugin API.
|
|
922
|
+
const result = run(opencodeBinary(), ["--version"]);
|
|
923
|
+
if (!versionAtLeast(result.stdout, [1, 18, 18]) || versionAtLeast(result.stdout, [2, 0, 0])) {
|
|
924
|
+
fail(`OpenCode >=1.18.18 and <2.0.0 is required; got ${result.stdout.trim()}`);
|
|
925
|
+
}
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
776
928
|
if (host === "claude") {
|
|
777
929
|
const result = run(claudeBinary(), ["--version"]);
|
|
778
930
|
if (!versionAtLeast(result.stdout, [2, 1, 205])) {
|
|
779
|
-
fail(
|
|
780
|
-
`Claude Code 2.1.205 or later is required for structured selector output; got ${result.stdout.trim()}`,
|
|
781
|
-
);
|
|
931
|
+
fail(`Claude Code 2.1.205 or later is required for structured selector output; got ${result.stdout.trim()}`);
|
|
782
932
|
}
|
|
783
933
|
if (authenticated) {
|
|
784
934
|
run(claudeBinary(), ["auth", "status"]);
|
|
@@ -792,6 +942,12 @@ function requireHostCli(host, { authenticated = false } = {}) {
|
|
|
792
942
|
}
|
|
793
943
|
|
|
794
944
|
function marketplaceEntries(host) {
|
|
945
|
+
if (FILE_DROP_HOSTS.includes(host) || host === "opencode") {
|
|
946
|
+
const paths = managedPaths(host);
|
|
947
|
+
const present =
|
|
948
|
+
existsSync(paths.root) || (host === "opencode" && (existsSync(paths.bridge) || existsSync(paths.bridgeMarker)));
|
|
949
|
+
return present ? [{ name: MARKETPLACE_NAME, root: paths.root }] : [];
|
|
950
|
+
}
|
|
795
951
|
const payload =
|
|
796
952
|
host === "claude"
|
|
797
953
|
? runClaudeJson(["plugin", "marketplace", "list", "--json"])
|
|
@@ -812,7 +968,124 @@ function marketplaceEntry(host) {
|
|
|
812
968
|
return matches[0] ?? null;
|
|
813
969
|
}
|
|
814
970
|
|
|
815
|
-
|
|
971
|
+
// Read Grok Build's machine-readable plugin state.
|
|
972
|
+
//
|
|
973
|
+
// The managed Grok files are host-independent content, so inspecting or
|
|
974
|
+
// removing what the installer owns must not depend on a runnable `grok`
|
|
975
|
+
// command: a user who uninstalls Grok Build still has to be able to clean up.
|
|
976
|
+
// Transactional callers pass requireHostState so that install and update keep
|
|
977
|
+
// confirming activation against the host itself.
|
|
978
|
+
function grokInspection(requireHostState) {
|
|
979
|
+
if (requireHostState) return runGrokJson(["inspect", "--json"]);
|
|
980
|
+
const result = run(grokBinary(), ["inspect", "--json"], { allowFailure: true });
|
|
981
|
+
if (result.error || result.status !== 0) {
|
|
982
|
+
const detail = result.error?.message || result.stderr?.trim() || `exit ${result.status}`;
|
|
983
|
+
console.warn(
|
|
984
|
+
`warning: could not read Grok Build plugin state (${detail}); ` +
|
|
985
|
+
"reporting the managed files only",
|
|
986
|
+
);
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
try {
|
|
990
|
+
const payload = JSON.parse(result.stdout);
|
|
991
|
+
if (payload === null || typeof payload !== "object") {
|
|
992
|
+
fail("Grok Build returned a non-container JSON value");
|
|
993
|
+
}
|
|
994
|
+
return payload;
|
|
995
|
+
} catch (error) {
|
|
996
|
+
if (error instanceof InstallerError) throw error;
|
|
997
|
+
fail(`Grok Build returned invalid JSON for inspect --json: ${error.message}`);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function pluginState(host, { requireHostState = false } = {}) {
|
|
1002
|
+
if (FILE_DROP_HOSTS.includes(host)) {
|
|
1003
|
+
const paths = managedPaths(host);
|
|
1004
|
+
if (!existsSync(paths.root)) return { kind: "missing", version: null };
|
|
1005
|
+
let manifest;
|
|
1006
|
+
try {
|
|
1007
|
+
manifest = JSON.parse(readFileSync(join(paths.root, "plugin.json"), "utf8"));
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
fail(`${host} plugin manifest is unreadable: ${error.message}`);
|
|
1010
|
+
}
|
|
1011
|
+
if (
|
|
1012
|
+
manifest === null ||
|
|
1013
|
+
typeof manifest !== "object" ||
|
|
1014
|
+
Array.isArray(manifest) ||
|
|
1015
|
+
(host === "cursor" &&
|
|
1016
|
+
manifest.$schema !== "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json") ||
|
|
1017
|
+
(host === "grok" && manifest.skills !== "./skills") ||
|
|
1018
|
+
manifest.name !== PLUGIN_NAME ||
|
|
1019
|
+
typeof manifest.version !== "string" ||
|
|
1020
|
+
manifest.version.trim().length === 0
|
|
1021
|
+
) {
|
|
1022
|
+
fail(`${host} reported an invalid OpenSocrates plugin manifest`);
|
|
1023
|
+
}
|
|
1024
|
+
if (host !== "grok") return { kind: "installed", version: manifest.version };
|
|
1025
|
+
const payload = grokInspection(requireHostState);
|
|
1026
|
+
if (payload === null) return { kind: "installed", version: manifest.version };
|
|
1027
|
+
if (!Array.isArray(payload.plugins)) {
|
|
1028
|
+
fail("Grok Build inspect returned an unexpected plugin schema");
|
|
1029
|
+
}
|
|
1030
|
+
const matches = payload.plugins.filter((entry) => entry?.name === PLUGIN_NAME);
|
|
1031
|
+
if (matches.length !== 1) {
|
|
1032
|
+
fail(
|
|
1033
|
+
matches.length === 0
|
|
1034
|
+
? "Grok Build did not discover the managed OpenSocrates plugin"
|
|
1035
|
+
: "Grok Build reported duplicate OpenSocrates plugins",
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
const entry = matches[0];
|
|
1039
|
+
if (
|
|
1040
|
+
typeof entry.enabled !== "boolean" ||
|
|
1041
|
+
typeof entry.path !== "string" ||
|
|
1042
|
+
entry.provides?.skills !== 1 ||
|
|
1043
|
+
entry.provides?.agents !== 0 ||
|
|
1044
|
+
entry.provides?.hooks !== false ||
|
|
1045
|
+
entry.provides?.mcpServers !== 0
|
|
1046
|
+
) {
|
|
1047
|
+
fail("Grok Build reported an invalid OpenSocrates plugin state");
|
|
1048
|
+
}
|
|
1049
|
+
let observedPath = resolve(entry.path);
|
|
1050
|
+
try {
|
|
1051
|
+
observedPath = realpathSync(observedPath);
|
|
1052
|
+
} catch {
|
|
1053
|
+
// The managed root exists here, so a missing reported path is invalid below.
|
|
1054
|
+
}
|
|
1055
|
+
let expectedPath = resolve(paths.root);
|
|
1056
|
+
try {
|
|
1057
|
+
expectedPath = realpathSync(expectedPath);
|
|
1058
|
+
} catch {
|
|
1059
|
+
// Ownership checks report a more specific error for a missing root.
|
|
1060
|
+
}
|
|
1061
|
+
if (observedPath !== expectedPath) {
|
|
1062
|
+
fail(`Grok Build resolved OpenSocrates from an unmanaged location: ${observedPath}`);
|
|
1063
|
+
}
|
|
1064
|
+
return { kind: entry.enabled ? "installed" : "disabled", version: manifest.version };
|
|
1065
|
+
}
|
|
1066
|
+
if (host === "opencode") {
|
|
1067
|
+
const paths = managedPaths(host);
|
|
1068
|
+
if (!existsSync(paths.root) && !existsSync(paths.bridge) && !existsSync(paths.bridgeMarker)) {
|
|
1069
|
+
return { kind: "missing", version: null };
|
|
1070
|
+
}
|
|
1071
|
+
let manifest;
|
|
1072
|
+
try {
|
|
1073
|
+
manifest = JSON.parse(readFileSync(join(paths.root, ".opensocrates-package", "opencode-plugin.json"), "utf8"));
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
fail(`opencode installed manifest is unreadable: ${error.message}`);
|
|
1076
|
+
}
|
|
1077
|
+
if (
|
|
1078
|
+
manifest === null ||
|
|
1079
|
+
typeof manifest !== "object" ||
|
|
1080
|
+
Array.isArray(manifest) ||
|
|
1081
|
+
manifest.name !== PLUGIN_NAME ||
|
|
1082
|
+
typeof manifest.version !== "string" ||
|
|
1083
|
+
manifest.version.trim().length === 0
|
|
1084
|
+
) {
|
|
1085
|
+
fail("opencode reported an invalid OpenSocrates installation manifest");
|
|
1086
|
+
}
|
|
1087
|
+
return { kind: "installed", version: manifest.version };
|
|
1088
|
+
}
|
|
816
1089
|
if (host === "claude") {
|
|
817
1090
|
const installed = claudePluginEntries(runClaudeJson(["plugin", "list", "--json"]));
|
|
818
1091
|
const matches = installed.filter((entry) => entry?.id === PLUGIN_ID);
|
|
@@ -831,9 +1104,7 @@ function pluginState(host) {
|
|
|
831
1104
|
version: entry.version ?? null,
|
|
832
1105
|
};
|
|
833
1106
|
}
|
|
834
|
-
const payload = runCodexJson([
|
|
835
|
-
"plugin", "list", "--marketplace", MARKETPLACE_NAME, "--available", "--json",
|
|
836
|
-
]);
|
|
1107
|
+
const payload = runCodexJson(["plugin", "list", "--marketplace", MARKETPLACE_NAME, "--available", "--json"]);
|
|
837
1108
|
const installed = Array.isArray(payload.installed) ? payload.installed : null;
|
|
838
1109
|
const available = Array.isArray(payload.available) ? payload.available : null;
|
|
839
1110
|
if (installed === null || available === null) {
|
|
@@ -845,10 +1116,16 @@ function pluginState(host) {
|
|
|
845
1116
|
fail(`Codex reported duplicate entries for ${PLUGIN_ID}`);
|
|
846
1117
|
}
|
|
847
1118
|
if (installedMatches.length === 1) {
|
|
848
|
-
return {
|
|
1119
|
+
return {
|
|
1120
|
+
kind: "installed",
|
|
1121
|
+
version: installedMatches[0].version ?? null,
|
|
1122
|
+
};
|
|
849
1123
|
}
|
|
850
1124
|
if (availableMatches.length === 1) {
|
|
851
|
-
return {
|
|
1125
|
+
return {
|
|
1126
|
+
kind: "available",
|
|
1127
|
+
version: availableMatches[0].version ?? null,
|
|
1128
|
+
};
|
|
852
1129
|
}
|
|
853
1130
|
return { kind: "missing", version: null };
|
|
854
1131
|
}
|
|
@@ -861,13 +1138,12 @@ function detectLegacyClaudeInstallation() {
|
|
|
861
1138
|
entry.name.toLowerCase() === MARKETPLACE_NAME,
|
|
862
1139
|
);
|
|
863
1140
|
const plugins = claudePluginEntries(runClaudeJson(["plugin", "list", "--json"]));
|
|
864
|
-
const legacyPlugins = plugins.filter(
|
|
865
|
-
(entry) =>
|
|
866
|
-
entry.id !== PLUGIN_ID &&
|
|
867
|
-
entry.id.toLowerCase() === PLUGIN_ID,
|
|
868
|
-
);
|
|
1141
|
+
const legacyPlugins = plugins.filter((entry) => entry.id !== PLUGIN_ID && entry.id.toLowerCase() === PLUGIN_ID);
|
|
869
1142
|
const names = marketplaces.map((entry) => entry.name).join(", ") || "OpenSocrates";
|
|
870
|
-
return {
|
|
1143
|
+
return {
|
|
1144
|
+
found: marketplaces.length > 0 || legacyPlugins.length > 0,
|
|
1145
|
+
names,
|
|
1146
|
+
};
|
|
871
1147
|
}
|
|
872
1148
|
|
|
873
1149
|
// Install and update write into the managed root, so a case-variant pre-1.0
|
|
@@ -932,6 +1208,85 @@ async function sha256File(target) {
|
|
|
932
1208
|
return hash.digest("hex");
|
|
933
1209
|
}
|
|
934
1210
|
|
|
1211
|
+
function openCodeSidecar(bridgeSha256) {
|
|
1212
|
+
return {
|
|
1213
|
+
...OPENCODE_MARKER,
|
|
1214
|
+
version: PRODUCT_VERSION,
|
|
1215
|
+
bridgeSha256,
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
async function ensureSafeDirectory(target, label) {
|
|
1220
|
+
if (await exists(target)) {
|
|
1221
|
+
const info = await lstat(target);
|
|
1222
|
+
if (!info.isDirectory() || info.isSymbolicLink()) {
|
|
1223
|
+
fail(`refusing to use an unsafe ${label} directory: ${target}`);
|
|
1224
|
+
}
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
await mkdir(target, { recursive: true, mode: 0o700 });
|
|
1228
|
+
const info = await lstat(target);
|
|
1229
|
+
if (!info.isDirectory() || info.isSymbolicLink()) {
|
|
1230
|
+
fail(`refusing to use an unsafe ${label} directory: ${target}`);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
async function ensureOpenCodeDirectories(paths) {
|
|
1235
|
+
await ensureSafeDirectory(paths.hostHome, "OpenCode configuration");
|
|
1236
|
+
await ensureSafeDirectory(paths.parent, "OpenCode skills");
|
|
1237
|
+
await ensureSafeDirectory(paths.bridgeParent, "OpenCode plugins");
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
async function verifyOpenCodeInstallation(paths) {
|
|
1241
|
+
await requireOwnedRoot(paths.root, "opencode");
|
|
1242
|
+
for (const [target, label] of [
|
|
1243
|
+
[paths.bridge, "plugin bridge"],
|
|
1244
|
+
[paths.bridgeMarker, "plugin ownership sidecar"],
|
|
1245
|
+
]) {
|
|
1246
|
+
if (!(await exists(target))) fail(`OpenCode ${label} is missing: ${target}`);
|
|
1247
|
+
const info = await lstat(target);
|
|
1248
|
+
if (!info.isFile() || info.isSymbolicLink()) {
|
|
1249
|
+
fail(`OpenCode ${label} is not an owned regular file: ${target}`);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
const sidecar = await readJsonObject(paths.bridgeMarker);
|
|
1253
|
+
const sidecarMarker = Object.fromEntries(Object.keys(OPENCODE_MARKER).map((key) => [key, sidecar[key]]));
|
|
1254
|
+
if (
|
|
1255
|
+
!markerMatches(sidecarMarker, "opencode") ||
|
|
1256
|
+
sidecar.version !== PRODUCT_VERSION ||
|
|
1257
|
+
typeof sidecar.bridgeSha256 !== "string" ||
|
|
1258
|
+
sidecar.bridgeSha256 !== (await sha256File(paths.bridge))
|
|
1259
|
+
) {
|
|
1260
|
+
fail("OpenCode plugin bridge has an invalid ownership sidecar or checksum");
|
|
1261
|
+
}
|
|
1262
|
+
const manifest = await readJsonObject(join(paths.root, OPENCODE_INSTALL_MANIFEST));
|
|
1263
|
+
if (
|
|
1264
|
+
manifest.schema !== "opensocrates.opencode-installation/1.0.0" ||
|
|
1265
|
+
manifest.version !== PRODUCT_VERSION ||
|
|
1266
|
+
manifest.bridgeSha256 !== sidecar.bridgeSha256 ||
|
|
1267
|
+
manifest.files === null ||
|
|
1268
|
+
typeof manifest.files !== "object" ||
|
|
1269
|
+
Array.isArray(manifest.files)
|
|
1270
|
+
) {
|
|
1271
|
+
fail("OpenCode installation inventory is invalid");
|
|
1272
|
+
}
|
|
1273
|
+
const actualFiles = new Set(await walkFiles(paths.root));
|
|
1274
|
+
actualFiles.delete(OPENCODE_INSTALL_MANIFEST);
|
|
1275
|
+
const declaredFiles = Object.keys(manifest.files);
|
|
1276
|
+
if (
|
|
1277
|
+
actualFiles.size !== declaredFiles.length ||
|
|
1278
|
+
declaredFiles.some((item) => !isSafeArchivePath(item) || !actualFiles.has(item))
|
|
1279
|
+
) {
|
|
1280
|
+
fail("OpenCode installation inventory does not cover the complete managed skill");
|
|
1281
|
+
}
|
|
1282
|
+
for (const item of declaredFiles) {
|
|
1283
|
+
if (manifest.files[item] !== (await sha256File(join(paths.root, ...item.split("/"))))) {
|
|
1284
|
+
fail(`OpenCode installed file checksum mismatch for ${item}`);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
return manifest;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
935
1290
|
async function downloadFile(url, destination) {
|
|
936
1291
|
const response = await fetch(url, {
|
|
937
1292
|
headers: {
|
|
@@ -962,6 +1317,11 @@ function localAssetInputs(options, host) {
|
|
|
962
1317
|
return { asset: options.asset, checksum: options.checksum };
|
|
963
1318
|
}
|
|
964
1319
|
|
|
1320
|
+
function qualifiedAssetHosts(options) {
|
|
1321
|
+
if (options.host !== ALL_HOST) return [];
|
|
1322
|
+
return SUPPORTED_HOSTS.filter((host) => options.hostAssets[host].asset !== null);
|
|
1323
|
+
}
|
|
1324
|
+
|
|
965
1325
|
async function resolveAssetInputs(options, scratch, host) {
|
|
966
1326
|
const local = localAssetInputs(options, host);
|
|
967
1327
|
if (local.asset !== null) {
|
|
@@ -1063,10 +1423,7 @@ async function verifyPackageChecksums(pluginRoot) {
|
|
|
1063
1423
|
}
|
|
1064
1424
|
}
|
|
1065
1425
|
const actualFiles = new Set((await walkFiles(pluginRoot)).filter((item) => item !== "checksums.sha256"));
|
|
1066
|
-
if (
|
|
1067
|
-
actualFiles.size !== declared.size ||
|
|
1068
|
-
[...actualFiles].some((item) => !declared.has(item))
|
|
1069
|
-
) {
|
|
1426
|
+
if (actualFiles.size !== declared.size || [...actualFiles].some((item) => !declared.has(item))) {
|
|
1070
1427
|
fail("package checksum manifest does not cover the complete archive");
|
|
1071
1428
|
}
|
|
1072
1429
|
return declared.size;
|
|
@@ -1088,16 +1445,36 @@ async function verifyExtractedPackage(pluginRoot, host) {
|
|
|
1088
1445
|
) {
|
|
1089
1446
|
fail("package release manifest does not match this installer");
|
|
1090
1447
|
}
|
|
1091
|
-
|
|
1092
|
-
pluginRoot,
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1448
|
+
if (HOST_LAYOUTS[host].requiresRuntime) {
|
|
1449
|
+
const runtime = join(pluginRoot, "runtime", "darwin-arm64", "opensocrates-runtime", "opensocrates-runtime");
|
|
1450
|
+
const runtimeInfo = await stat(runtime);
|
|
1451
|
+
if (!runtimeInfo.isFile() || (runtimeInfo.mode & 0o111) === 0) {
|
|
1452
|
+
fail("package is missing the executable darwin-arm64 runtime");
|
|
1453
|
+
}
|
|
1454
|
+
} else if (
|
|
1455
|
+
release.launchers?.length !== 0 ||
|
|
1456
|
+
release.runtime_targets?.length !== 0 ||
|
|
1457
|
+
(await exists(join(pluginRoot, "runtime"))) ||
|
|
1458
|
+
(await exists(join(pluginRoot, "hooks"))) ||
|
|
1459
|
+
(await exists(join(pluginRoot, "bin"))) ||
|
|
1460
|
+
((host === "cursor" || host === "grok") && (await exists(join(pluginRoot, "mcp.json")))) ||
|
|
1461
|
+
(host === "grok" &&
|
|
1462
|
+
((await exists(join(pluginRoot, "commands"))) ||
|
|
1463
|
+
(await exists(join(pluginRoot, "agents"))) ||
|
|
1464
|
+
(await exists(join(pluginRoot, ".mcp.json")))))
|
|
1465
|
+
) {
|
|
1466
|
+
fail(`${host} package contains an unexpected native runtime or launcher surface`);
|
|
1467
|
+
}
|
|
1468
|
+
if (host === "opencode") {
|
|
1469
|
+
if (
|
|
1470
|
+
manifest.minimum_opencode_version !== "1.18.18" ||
|
|
1471
|
+
manifest.stable_plugin_hook !== "chat.message" ||
|
|
1472
|
+
manifest.beta_v2_api !== false ||
|
|
1473
|
+
!(await exists(join(pluginRoot, "plugins", "opensocrates.js"))) ||
|
|
1474
|
+
!(await exists(join(pluginRoot, "skills", "opensocrates", "SKILL.md")))
|
|
1475
|
+
) {
|
|
1476
|
+
fail("OpenCode package does not match the verified stable bridge contract");
|
|
1477
|
+
}
|
|
1101
1478
|
}
|
|
1102
1479
|
return verifyPackageChecksums(pluginRoot);
|
|
1103
1480
|
}
|
|
@@ -1121,17 +1498,11 @@ async function prepareVerifiedPackage(options, host = options.host) {
|
|
|
1121
1498
|
}
|
|
1122
1499
|
|
|
1123
1500
|
async function prepareVerifiedPackages(options, hosts) {
|
|
1124
|
-
const settled = await Promise.allSettled(
|
|
1125
|
-
|
|
1126
|
-
);
|
|
1127
|
-
const prepared = settled
|
|
1128
|
-
.filter((result) => result.status === "fulfilled")
|
|
1129
|
-
.map((result) => result.value);
|
|
1501
|
+
const settled = await Promise.allSettled(hosts.map((host) => prepareVerifiedPackage(options, host)));
|
|
1502
|
+
const prepared = settled.filter((result) => result.status === "fulfilled").map((result) => result.value);
|
|
1130
1503
|
const failure = settled.find((result) => result.status === "rejected");
|
|
1131
1504
|
if (failure) {
|
|
1132
|
-
await Promise.all(
|
|
1133
|
-
prepared.map((item) => rm(item.scratch, { recursive: true, force: true })),
|
|
1134
|
-
);
|
|
1505
|
+
await Promise.all(prepared.map((item) => rm(item.scratch, { recursive: true, force: true })));
|
|
1135
1506
|
throw failure.reason;
|
|
1136
1507
|
}
|
|
1137
1508
|
return prepared;
|
|
@@ -1150,8 +1521,7 @@ function expectedMarketplace(host) {
|
|
|
1150
1521
|
{
|
|
1151
1522
|
name: PLUGIN_NAME,
|
|
1152
1523
|
source: `./plugins/${PLUGIN_NAME}`,
|
|
1153
|
-
description:
|
|
1154
|
-
"Local reasoning-system selection for Claude Code and Cowork, plus one /opensocrates entry.",
|
|
1524
|
+
description: "Local reasoning-system selection for Claude Code and Cowork, plus one /opensocrates entry.",
|
|
1155
1525
|
category: "workflow",
|
|
1156
1526
|
},
|
|
1157
1527
|
],
|
|
@@ -1181,11 +1551,23 @@ async function buildStagingTree(parent, pluginSource, host) {
|
|
|
1181
1551
|
const layout = HOST_LAYOUTS[host];
|
|
1182
1552
|
const staging = await mkdtemp(join(parent, ".opensocrates.staging-"));
|
|
1183
1553
|
try {
|
|
1554
|
+
if (FILE_DROP_HOSTS.includes(host)) {
|
|
1555
|
+
await cp(pluginSource, staging, { recursive: true, preserveTimestamps: true });
|
|
1556
|
+
await verifyExtractedPackage(staging, host);
|
|
1557
|
+
await writeFile(join(staging, MARKER_NAME), `${JSON.stringify(markerFor(host), null, 2)}\n`, {
|
|
1558
|
+
encoding: "utf8",
|
|
1559
|
+
mode: 0o600,
|
|
1560
|
+
});
|
|
1561
|
+
return staging;
|
|
1562
|
+
}
|
|
1184
1563
|
const marketplace = join(staging, layout.marketplaceRelative);
|
|
1185
1564
|
const plugin = join(staging, layout.pluginRelative);
|
|
1186
1565
|
await mkdir(dirname(marketplace), { recursive: true, mode: 0o700 });
|
|
1187
1566
|
await mkdir(dirname(plugin), { recursive: true, mode: 0o700 });
|
|
1188
|
-
await cp(pluginSource, plugin, {
|
|
1567
|
+
await cp(pluginSource, plugin, {
|
|
1568
|
+
recursive: true,
|
|
1569
|
+
preserveTimestamps: true,
|
|
1570
|
+
});
|
|
1189
1571
|
await writeFile(marketplace, `${JSON.stringify(expectedMarketplace(host), null, 2)}\n`, {
|
|
1190
1572
|
encoding: "utf8",
|
|
1191
1573
|
mode: 0o600,
|
|
@@ -1202,20 +1584,72 @@ async function buildStagingTree(parent, pluginSource, host) {
|
|
|
1202
1584
|
}
|
|
1203
1585
|
}
|
|
1204
1586
|
|
|
1587
|
+
async function buildOpenCodeStaging(paths, pluginSource) {
|
|
1588
|
+
await ensureOpenCodeDirectories(paths);
|
|
1589
|
+
const root = await mkdtemp(join(paths.parent, ".opensocrates.staging-"));
|
|
1590
|
+
const bridge = join(paths.bridgeParent, `.opensocrates.js.staging-${randomUUID()}`);
|
|
1591
|
+
const bridgeMarker = join(paths.bridgeParent, `.opensocrates-managed.json.staging-${randomUUID()}`);
|
|
1592
|
+
try {
|
|
1593
|
+
await cp(join(pluginSource, "skills", "opensocrates"), root, {
|
|
1594
|
+
recursive: true,
|
|
1595
|
+
preserveTimestamps: true,
|
|
1596
|
+
});
|
|
1597
|
+
const metadata = join(root, ".opensocrates-package");
|
|
1598
|
+
await mkdir(metadata, { mode: 0o700 });
|
|
1599
|
+
for (const name of ["opencode-plugin.json", "release-manifest.json", "checksums.sha256"]) {
|
|
1600
|
+
await cp(join(pluginSource, name), join(metadata, name), {
|
|
1601
|
+
preserveTimestamps: true,
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
await writeFile(join(root, MARKER_NAME), `${JSON.stringify(markerFor("opencode"), null, 2)}\n`, {
|
|
1605
|
+
encoding: "utf8",
|
|
1606
|
+
mode: 0o600,
|
|
1607
|
+
});
|
|
1608
|
+
await cp(join(pluginSource, "plugins", "opensocrates.js"), bridge, {
|
|
1609
|
+
preserveTimestamps: true,
|
|
1610
|
+
});
|
|
1611
|
+
const bridgeSha256 = await sha256File(bridge);
|
|
1612
|
+
const files = {};
|
|
1613
|
+
for (const item of (await walkFiles(root)).sort()) {
|
|
1614
|
+
files[item] = await sha256File(join(root, ...item.split("/")));
|
|
1615
|
+
}
|
|
1616
|
+
await writeFile(
|
|
1617
|
+
join(root, OPENCODE_INSTALL_MANIFEST),
|
|
1618
|
+
`${JSON.stringify(
|
|
1619
|
+
{
|
|
1620
|
+
schema: "opensocrates.opencode-installation/1.0.0",
|
|
1621
|
+
version: PRODUCT_VERSION,
|
|
1622
|
+
bridgeSha256,
|
|
1623
|
+
files,
|
|
1624
|
+
},
|
|
1625
|
+
null,
|
|
1626
|
+
2,
|
|
1627
|
+
)}\n`,
|
|
1628
|
+
{ encoding: "utf8", mode: 0o600 },
|
|
1629
|
+
);
|
|
1630
|
+
await writeFile(bridgeMarker, `${JSON.stringify(openCodeSidecar(bridgeSha256), null, 2)}\n`, {
|
|
1631
|
+
encoding: "utf8",
|
|
1632
|
+
mode: 0o600,
|
|
1633
|
+
});
|
|
1634
|
+
return { root, bridge, bridgeMarker };
|
|
1635
|
+
} catch (error) {
|
|
1636
|
+
await rm(root, { recursive: true, force: true });
|
|
1637
|
+
await rm(bridge, { force: true });
|
|
1638
|
+
await rm(bridgeMarker, { force: true });
|
|
1639
|
+
throw error;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1205
1643
|
function entryRoot(entry, host) {
|
|
1206
1644
|
if (entry === null) {
|
|
1207
1645
|
return null;
|
|
1208
1646
|
}
|
|
1209
1647
|
let value;
|
|
1210
1648
|
if (host === "claude") {
|
|
1211
|
-
const fields = ["path", "installLocation"].filter(
|
|
1212
|
-
(field) => entry[field] !== undefined && entry[field] !== null,
|
|
1213
|
-
);
|
|
1649
|
+
const fields = ["path", "installLocation"].filter((field) => entry[field] !== undefined && entry[field] !== null);
|
|
1214
1650
|
if (
|
|
1215
1651
|
fields.length === 0 ||
|
|
1216
|
-
fields.some(
|
|
1217
|
-
(field) => typeof entry[field] !== "string" || entry[field].trim().length === 0,
|
|
1218
|
-
)
|
|
1652
|
+
fields.some((field) => typeof entry[field] !== "string" || entry[field].trim().length === 0)
|
|
1219
1653
|
) {
|
|
1220
1654
|
fail(`${host} marketplace ${MARKETPLACE_NAME} has no usable root`);
|
|
1221
1655
|
}
|
|
@@ -1250,14 +1684,13 @@ function entryRoot(entry, host) {
|
|
|
1250
1684
|
}
|
|
1251
1685
|
|
|
1252
1686
|
function removeRegistration(host, entry, state) {
|
|
1687
|
+
if (FILE_DROP_HOSTS.includes(host) || host === "opencode") return;
|
|
1253
1688
|
if (host === "claude") {
|
|
1254
1689
|
if (["installed", "disabled"].includes(state.kind)) {
|
|
1255
1690
|
run(claudeBinary(), ["plugin", "uninstall", PLUGIN_ID, "--scope", "user"]);
|
|
1256
1691
|
}
|
|
1257
1692
|
if (entry !== null) {
|
|
1258
|
-
run(claudeBinary(), [
|
|
1259
|
-
"plugin", "marketplace", "remove", MARKETPLACE_NAME, "--scope", "user",
|
|
1260
|
-
]);
|
|
1693
|
+
run(claudeBinary(), ["plugin", "marketplace", "remove", MARKETPLACE_NAME, "--scope", "user"]);
|
|
1261
1694
|
}
|
|
1262
1695
|
return;
|
|
1263
1696
|
}
|
|
@@ -1270,6 +1703,7 @@ function removeRegistration(host, entry, state) {
|
|
|
1270
1703
|
}
|
|
1271
1704
|
|
|
1272
1705
|
function addRegistration(host, root, installPlugin, { enabled = true } = {}) {
|
|
1706
|
+
if (FILE_DROP_HOSTS.includes(host) || host === "opencode") return null;
|
|
1273
1707
|
if (host === "claude") {
|
|
1274
1708
|
run(claudeBinary(), ["plugin", "marketplace", "add", root, "--scope", "user"]);
|
|
1275
1709
|
if (installPlugin) {
|
|
@@ -1288,6 +1722,7 @@ function addRegistration(host, root, installPlugin, { enabled = true } = {}) {
|
|
|
1288
1722
|
}
|
|
1289
1723
|
|
|
1290
1724
|
function removeRegistrationBestEffort(host) {
|
|
1725
|
+
if (FILE_DROP_HOSTS.includes(host) || host === "opencode") return;
|
|
1291
1726
|
const entry = (() => {
|
|
1292
1727
|
try {
|
|
1293
1728
|
return marketplaceEntry(host);
|
|
@@ -1324,15 +1759,19 @@ async function recoveryStep(label, action) {
|
|
|
1324
1759
|
await action();
|
|
1325
1760
|
return true;
|
|
1326
1761
|
} catch (recoveryError) {
|
|
1327
|
-
const detail =
|
|
1328
|
-
recoveryError instanceof Error ? recoveryError.message : String(recoveryError);
|
|
1762
|
+
const detail = recoveryError instanceof Error ? recoveryError.message : String(recoveryError);
|
|
1329
1763
|
console.error(`warning: rollback step failed (${label}): ${detail}`);
|
|
1330
1764
|
return false;
|
|
1331
1765
|
}
|
|
1332
1766
|
}
|
|
1333
1767
|
|
|
1334
1768
|
async function preflightHost(host, action) {
|
|
1335
|
-
|
|
1769
|
+
if (
|
|
1770
|
+
(!FILE_DROP_HOSTS.includes(host) && host !== "opencode") ||
|
|
1771
|
+
["install", "update"].includes(action)
|
|
1772
|
+
) {
|
|
1773
|
+
requireHostCli(host, { authenticated: ["install", "update"].includes(action) });
|
|
1774
|
+
}
|
|
1336
1775
|
if (host === "claude") {
|
|
1337
1776
|
if (["install", "update"].includes(action)) {
|
|
1338
1777
|
requireNoLegacyClaudeInstallation();
|
|
@@ -1341,6 +1780,22 @@ async function preflightHost(host, action) {
|
|
|
1341
1780
|
}
|
|
1342
1781
|
}
|
|
1343
1782
|
const paths = managedPaths(host);
|
|
1783
|
+
if (host === "opencode") {
|
|
1784
|
+
const presence = await Promise.all([paths.root, paths.bridge, paths.bridgeMarker].map((target) => exists(target)));
|
|
1785
|
+
if (presence.some(Boolean) && !presence.every(Boolean)) {
|
|
1786
|
+
fail("OpenCode has a partial or unowned OpenSocrates installation; refusing to replace it");
|
|
1787
|
+
}
|
|
1788
|
+
if (presence.every(Boolean)) await verifyOpenCodeInstallation(paths);
|
|
1789
|
+
const previousEntry = presence.every(Boolean) ? { name: MARKETPLACE_NAME, root: paths.root } : null;
|
|
1790
|
+
const previousState = previousEntry === null ? { kind: "missing", version: null } : pluginState(host);
|
|
1791
|
+
return {
|
|
1792
|
+
host,
|
|
1793
|
+
paths,
|
|
1794
|
+
previousEntry,
|
|
1795
|
+
previousState,
|
|
1796
|
+
rootExists: presence[0],
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1344
1799
|
const previousEntry = marketplaceEntry(host);
|
|
1345
1800
|
if (previousEntry !== null && entryRoot(previousEntry, host) !== paths.root) {
|
|
1346
1801
|
fail(
|
|
@@ -1356,7 +1811,9 @@ async function preflightHost(host, action) {
|
|
|
1356
1811
|
fail(`${host} has a managed registration whose root is missing: ${paths.root}`);
|
|
1357
1812
|
}
|
|
1358
1813
|
const previousState =
|
|
1359
|
-
previousEntry === null
|
|
1814
|
+
previousEntry === null
|
|
1815
|
+
? { kind: "missing", version: null }
|
|
1816
|
+
: pluginState(host, { requireHostState: ["install", "update"].includes(action) });
|
|
1360
1817
|
return { host, paths, previousEntry, previousState, rootExists };
|
|
1361
1818
|
}
|
|
1362
1819
|
|
|
@@ -1364,32 +1821,39 @@ async function preflightSelectedHosts(options, action, desiredState) {
|
|
|
1364
1821
|
if (options.host !== ALL_HOST) {
|
|
1365
1822
|
return [await preflightHost(options.host, action)];
|
|
1366
1823
|
}
|
|
1824
|
+
// A pre-release or offline caller may supply only the exact host archives it
|
|
1825
|
+
// intends to transact. Never mix those candidate packages with downloads
|
|
1826
|
+
// for other locally available hosts: the qualified asset set is the exact
|
|
1827
|
+
// transaction boundary. With no local assets, --host all retains discovery
|
|
1828
|
+
// of every ready host.
|
|
1829
|
+
const assetHosts = qualifiedAssetHosts(options);
|
|
1830
|
+
const candidates = assetHosts.length > 0 ? assetHosts : SUPPORTED_HOSTS;
|
|
1367
1831
|
const desiredHosts = new Set(desiredState.installedHosts);
|
|
1368
1832
|
const rootPresence = Object.fromEntries(
|
|
1369
|
-
await Promise.all(
|
|
1370
|
-
SUPPORTED_HOSTS.map(async (host) => [host, await exists(managedPaths(host).root)]),
|
|
1371
|
-
),
|
|
1372
|
-
);
|
|
1373
|
-
const settled = await Promise.allSettled(
|
|
1374
|
-
SUPPORTED_HOSTS.map((host) => preflightHost(host, action)),
|
|
1833
|
+
await Promise.all(candidates.map(async (host) => [host, await exists(managedPaths(host).root)])),
|
|
1375
1834
|
);
|
|
1835
|
+
const settled = await Promise.allSettled(candidates.map((host) => preflightHost(host, action)));
|
|
1376
1836
|
const successes = new Map();
|
|
1377
1837
|
const failures = new Map();
|
|
1378
1838
|
settled.forEach((result, index) => {
|
|
1379
|
-
const host =
|
|
1839
|
+
const host = candidates[index];
|
|
1380
1840
|
if (result.status === "fulfilled") successes.set(host, result.value);
|
|
1381
1841
|
else failures.set(host, result.reason);
|
|
1382
1842
|
});
|
|
1383
1843
|
|
|
1384
1844
|
const required = new Set();
|
|
1385
|
-
if (
|
|
1386
|
-
for (const host of
|
|
1845
|
+
if (assetHosts.length > 0) {
|
|
1846
|
+
for (const host of assetHosts) required.add(host);
|
|
1847
|
+
} else if (action === "install") {
|
|
1848
|
+
for (const host of candidates) {
|
|
1387
1849
|
if (desiredHosts.has(host) || rootPresence[host]) required.add(host);
|
|
1388
1850
|
}
|
|
1389
1851
|
} else if (desiredHosts.size > 0) {
|
|
1390
|
-
for (const host of desiredHosts)
|
|
1852
|
+
for (const host of desiredHosts) {
|
|
1853
|
+
if (candidates.includes(host)) required.add(host);
|
|
1854
|
+
}
|
|
1391
1855
|
} else if (action === "remove") {
|
|
1392
|
-
for (const host of
|
|
1856
|
+
for (const host of candidates) {
|
|
1393
1857
|
if (rootPresence[host]) required.add(host);
|
|
1394
1858
|
}
|
|
1395
1859
|
}
|
|
@@ -1401,7 +1865,9 @@ async function preflightSelectedHosts(options, action, desiredState) {
|
|
|
1401
1865
|
}
|
|
1402
1866
|
|
|
1403
1867
|
let selected;
|
|
1404
|
-
if (
|
|
1868
|
+
if (assetHosts.length > 0) {
|
|
1869
|
+
selected = assetHosts.map((host) => successes.get(host)).filter(Boolean);
|
|
1870
|
+
} else if (action === "update" && desiredHosts.size > 0) {
|
|
1405
1871
|
selected = [...desiredHosts].map((host) => successes.get(host)).filter(Boolean);
|
|
1406
1872
|
} else if (action === "remove") {
|
|
1407
1873
|
selected = [...successes.values()].filter(
|
|
@@ -1427,14 +1893,26 @@ async function preflightSelectedHosts(options, action, desiredState) {
|
|
|
1427
1893
|
async function stageInstallation(preflight, pluginSource) {
|
|
1428
1894
|
const { host, paths } = preflight;
|
|
1429
1895
|
await mkdir(paths.parent, { recursive: true, mode: 0o700 });
|
|
1430
|
-
const
|
|
1896
|
+
const transient = transientParent(host, paths);
|
|
1897
|
+
await mkdir(transient, { recursive: true, mode: 0o700 });
|
|
1898
|
+
const staging =
|
|
1899
|
+
host === "opencode"
|
|
1900
|
+
? await buildOpenCodeStaging(paths, pluginSource)
|
|
1901
|
+
: await buildStagingTree(transient, pluginSource, host);
|
|
1431
1902
|
return {
|
|
1432
1903
|
...preflight,
|
|
1433
1904
|
staging,
|
|
1434
|
-
backup: join(
|
|
1905
|
+
backup: join(transient, `.opensocrates.backup-${randomUUID()}`),
|
|
1906
|
+
bridgeBackup: host === "opencode" ? join(paths.bridgeParent, `.opensocrates.js.backup-${randomUUID()}`) : null,
|
|
1907
|
+
bridgeMarkerBackup:
|
|
1908
|
+
host === "opencode" ? join(paths.bridgeParent, `.opensocrates-managed.json.backup-${randomUUID()}`) : null,
|
|
1435
1909
|
registrationRemoved: false,
|
|
1436
1910
|
backupCreated: false,
|
|
1437
1911
|
newRootActive: false,
|
|
1912
|
+
newBridgeActive: false,
|
|
1913
|
+
newBridgeMarkerActive: false,
|
|
1914
|
+
bridgeBackupCreated: false,
|
|
1915
|
+
bridgeMarkerBackupCreated: false,
|
|
1438
1916
|
activationStarted: false,
|
|
1439
1917
|
};
|
|
1440
1918
|
}
|
|
@@ -1450,13 +1928,27 @@ async function activateInstallation(transaction) {
|
|
|
1450
1928
|
await rename(paths.root, transaction.backup);
|
|
1451
1929
|
transaction.backupCreated = true;
|
|
1452
1930
|
}
|
|
1453
|
-
await
|
|
1931
|
+
if (host === "opencode" && (await exists(paths.bridge))) {
|
|
1932
|
+
await rename(paths.bridge, transaction.bridgeBackup);
|
|
1933
|
+
transaction.bridgeBackupCreated = true;
|
|
1934
|
+
}
|
|
1935
|
+
if (host === "opencode" && (await exists(paths.bridgeMarker))) {
|
|
1936
|
+
await rename(paths.bridgeMarker, transaction.bridgeMarkerBackup);
|
|
1937
|
+
transaction.bridgeMarkerBackupCreated = true;
|
|
1938
|
+
}
|
|
1939
|
+
await rename(host === "opencode" ? transaction.staging.root : transaction.staging, paths.root);
|
|
1454
1940
|
transaction.newRootActive = true;
|
|
1941
|
+
if (host === "opencode") {
|
|
1942
|
+
await rename(transaction.staging.bridge, paths.bridge);
|
|
1943
|
+
transaction.newBridgeActive = true;
|
|
1944
|
+
await rename(transaction.staging.bridgeMarker, paths.bridgeMarker);
|
|
1945
|
+
transaction.newBridgeMarkerActive = true;
|
|
1946
|
+
await verifyOpenCodeInstallation(paths);
|
|
1947
|
+
}
|
|
1455
1948
|
const result = addRegistration(host, paths.root, true);
|
|
1456
|
-
const state = pluginState(host);
|
|
1949
|
+
const state = pluginState(host, { requireHostState: true });
|
|
1457
1950
|
if (
|
|
1458
|
-
(host === "codex" &&
|
|
1459
|
-
(result?.pluginId !== PLUGIN_ID || result?.version !== PRODUCT_VERSION)) ||
|
|
1951
|
+
(host === "codex" && (result?.pluginId !== PLUGIN_ID || result?.version !== PRODUCT_VERSION)) ||
|
|
1460
1952
|
state.kind !== "installed" ||
|
|
1461
1953
|
state.version !== PRODUCT_VERSION
|
|
1462
1954
|
) {
|
|
@@ -1479,41 +1971,56 @@ async function rollbackInstallation(transaction) {
|
|
|
1479
1971
|
await rm(paths.root, { recursive: true, force: true });
|
|
1480
1972
|
});
|
|
1481
1973
|
}
|
|
1974
|
+
if (host === "opencode") {
|
|
1975
|
+
if (transaction.newBridgeActive && (await exists(paths.bridge))) {
|
|
1976
|
+
await recoveryStep("remove the failed OpenCode bridge", async () => {
|
|
1977
|
+
await rm(paths.bridge, { force: true });
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
if (transaction.newBridgeMarkerActive && (await exists(paths.bridgeMarker))) {
|
|
1981
|
+
await recoveryStep("remove the failed OpenCode bridge sidecar", async () => {
|
|
1982
|
+
await rm(paths.bridgeMarker, { force: true });
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
if (transaction.bridgeBackupCreated && (await exists(transaction.bridgeBackup))) {
|
|
1986
|
+
await recoveryStep("restore the previous OpenCode bridge", async () => {
|
|
1987
|
+
await rename(transaction.bridgeBackup, paths.bridge);
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
if (transaction.bridgeMarkerBackupCreated && (await exists(transaction.bridgeMarkerBackup))) {
|
|
1991
|
+
await recoveryStep("restore the previous OpenCode bridge sidecar", async () => {
|
|
1992
|
+
await rename(transaction.bridgeMarkerBackup, paths.bridgeMarker);
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1482
1996
|
if (transaction.backupCreated && (await exists(transaction.backup))) {
|
|
1483
1997
|
restored = await recoveryStep(`restore the previous ${host} installation`, async () => {
|
|
1484
1998
|
await requireOwnedRoot(transaction.backup, host);
|
|
1485
1999
|
await rename(transaction.backup, paths.root);
|
|
1486
2000
|
});
|
|
1487
2001
|
}
|
|
1488
|
-
if (
|
|
1489
|
-
transaction.registrationRemoved &&
|
|
1490
|
-
previousEntry !== null &&
|
|
1491
|
-
(restored || !transaction.backupCreated)
|
|
1492
|
-
) {
|
|
2002
|
+
if (transaction.registrationRemoved && previousEntry !== null && (restored || !transaction.backupCreated)) {
|
|
1493
2003
|
await recoveryStep(`re-register the previous ${host} installation`, async () => {
|
|
1494
|
-
addRegistration(
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
["installed", "disabled"].includes(previousState.kind),
|
|
1498
|
-
{ enabled: previousState.kind !== "disabled" },
|
|
1499
|
-
);
|
|
2004
|
+
addRegistration(host, paths.root, ["installed", "disabled"].includes(previousState.kind), {
|
|
2005
|
+
enabled: previousState.kind !== "disabled",
|
|
2006
|
+
});
|
|
1500
2007
|
});
|
|
1501
2008
|
}
|
|
1502
2009
|
if (transaction.backupCreated && !restored) {
|
|
1503
|
-
console.error(
|
|
1504
|
-
`error: the previous ${host} OpenSocrates installation could not be restored automatically.`,
|
|
1505
|
-
);
|
|
2010
|
+
console.error(`error: the previous ${host} OpenSocrates installation could not be restored automatically.`);
|
|
1506
2011
|
console.error(`error: your previous files are preserved at: ${transaction.backup}`);
|
|
1507
2012
|
console.error(`error: recovery command: /bin/rm -rf -- ${shellQuote(paths.root)}`);
|
|
1508
|
-
console.error(
|
|
1509
|
-
`error: recovery command: /bin/mv -- ${shellQuote(transaction.backup)} ${shellQuote(paths.root)}`,
|
|
1510
|
-
);
|
|
2013
|
+
console.error(`error: recovery command: /bin/mv -- ${shellQuote(transaction.backup)} ${shellQuote(paths.root)}`);
|
|
1511
2014
|
console.error(`error: recovery command: opensocrates install --host ${host}`);
|
|
1512
2015
|
}
|
|
1513
2016
|
}
|
|
1514
2017
|
|
|
1515
2018
|
async function cleanupInstallationTransaction(transaction) {
|
|
1516
|
-
if (
|
|
2019
|
+
if (transaction.host === "opencode") {
|
|
2020
|
+
await rm(transaction.staging.root, { recursive: true, force: true });
|
|
2021
|
+
await rm(transaction.staging.bridge, { force: true });
|
|
2022
|
+
await rm(transaction.staging.bridgeMarker, { force: true });
|
|
2023
|
+
} else if (await exists(transaction.staging)) {
|
|
1517
2024
|
await rm(transaction.staging, { recursive: true, force: true });
|
|
1518
2025
|
}
|
|
1519
2026
|
}
|
|
@@ -1524,6 +2031,14 @@ async function commitInstallation(transaction) {
|
|
|
1524
2031
|
await rm(transaction.backup, { recursive: true });
|
|
1525
2032
|
transaction.backupCreated = false;
|
|
1526
2033
|
}
|
|
2034
|
+
if (transaction.host === "opencode") {
|
|
2035
|
+
for (const [created, backup] of [
|
|
2036
|
+
[transaction.bridgeBackupCreated, transaction.bridgeBackup],
|
|
2037
|
+
[transaction.bridgeMarkerBackupCreated, transaction.bridgeMarkerBackup],
|
|
2038
|
+
]) {
|
|
2039
|
+
if (created && (await exists(backup))) await rm(backup);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
1527
2042
|
}
|
|
1528
2043
|
|
|
1529
2044
|
function installedDesiredState(previous, hosts) {
|
|
@@ -1546,8 +2061,7 @@ async function runInstallOrUpdate(options, action) {
|
|
|
1546
2061
|
for (const preflight of preflights) {
|
|
1547
2062
|
const item = packages.get(preflight.host);
|
|
1548
2063
|
console.log(
|
|
1549
|
-
`${preflight.host}: verified OpenSocrates ${PRODUCT_VERSION} and ` +
|
|
1550
|
-
`${item.checkedFiles} package files`,
|
|
2064
|
+
`${preflight.host}: verified OpenSocrates ${PRODUCT_VERSION} and ` + `${item.checkedFiles} package files`,
|
|
1551
2065
|
);
|
|
1552
2066
|
transactions.push(await stageInstallation(preflight, item.pluginRoot));
|
|
1553
2067
|
}
|
|
@@ -1563,9 +2077,7 @@ async function runInstallOrUpdate(options, action) {
|
|
|
1563
2077
|
throw error;
|
|
1564
2078
|
} finally {
|
|
1565
2079
|
await Promise.all(transactions.map(cleanupInstallationTransaction));
|
|
1566
|
-
await Promise.all(
|
|
1567
|
-
prepared.map((item) => rm(item.scratch, { recursive: true, force: true })),
|
|
1568
|
-
);
|
|
2080
|
+
await Promise.all(prepared.map((item) => rm(item.scratch, { recursive: true, force: true })));
|
|
1569
2081
|
}
|
|
1570
2082
|
for (const transaction of transactions) {
|
|
1571
2083
|
const cleaned = await recoveryStep(`remove the committed ${transaction.host} backup`, async () => {
|
|
@@ -1576,17 +2088,47 @@ async function runInstallOrUpdate(options, action) {
|
|
|
1576
2088
|
}
|
|
1577
2089
|
}
|
|
1578
2090
|
const verb = action === "update" ? "updated" : "installed";
|
|
1579
|
-
console.log(
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
2091
|
+
console.log(`OpenSocrates ${PRODUCT_VERSION} ${verb} successfully for ${hosts.join(", ")}.`);
|
|
2092
|
+
if (hosts.includes("codex")) {
|
|
2093
|
+
console.log(
|
|
2094
|
+
"Codex approval required: open one interactive Codex session and approve the " +
|
|
2095
|
+
"OpenSocrates hooks before relying on automatic selection. Non-interactive " +
|
|
2096
|
+
"codex exec silently skips hooks that have not been trusted.",
|
|
2097
|
+
);
|
|
2098
|
+
}
|
|
2099
|
+
if (hosts.includes("cursor")) {
|
|
2100
|
+
console.log(
|
|
2101
|
+
"Cursor: run Developer: Reload Window, then invoke /opensocrates from Agent chat. " +
|
|
2102
|
+
"This experimental package adds no automatic OpenSocrates hook selector.",
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
if (hosts.includes("grok")) {
|
|
2106
|
+
console.log(
|
|
2107
|
+
"Grok Build: start a new task for automatic native-skill selection, or invoke /opensocrates explicitly.",
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
2110
|
+
if (hosts.includes("opencode")) {
|
|
2111
|
+
console.log(
|
|
2112
|
+
"OpenCode: automatic same-turn selection uses the stable chat.message bridge; " +
|
|
2113
|
+
"the native opensocrates skill remains available as an explicit fallback.",
|
|
2114
|
+
);
|
|
2115
|
+
}
|
|
2116
|
+
console.log("Start new host tasks to load the updated OpenSocrates integration.");
|
|
1583
2117
|
return hosts;
|
|
1584
2118
|
}
|
|
1585
2119
|
|
|
1586
2120
|
async function inspectHostStatus(host) {
|
|
1587
|
-
requireHostCli(host);
|
|
2121
|
+
if (!FILE_DROP_HOSTS.includes(host) && host !== "opencode") requireHostCli(host);
|
|
1588
2122
|
if (host === "claude") warnLegacyClaudeInstallation();
|
|
1589
2123
|
const paths = managedPaths(host);
|
|
2124
|
+
if (host === "opencode") {
|
|
2125
|
+
const presence = await Promise.all([paths.root, paths.bridge, paths.bridgeMarker].map((target) => exists(target)));
|
|
2126
|
+
if (!presence.some(Boolean)) return { host, kind: "missing", version: null, paths };
|
|
2127
|
+
if (!presence.every(Boolean)) return { host, kind: "files-only", version: null, paths };
|
|
2128
|
+
await verifyOpenCodeInstallation(paths);
|
|
2129
|
+
const state = pluginState(host);
|
|
2130
|
+
return { host, kind: state.kind, version: state.version, paths };
|
|
2131
|
+
}
|
|
1590
2132
|
const entry = marketplaceEntry(host);
|
|
1591
2133
|
if (entry === null) {
|
|
1592
2134
|
if (await exists(paths.root)) {
|
|
@@ -1617,14 +2159,19 @@ async function showStatus(host) {
|
|
|
1617
2159
|
const status = await inspectHostStatus(candidate);
|
|
1618
2160
|
const expected = desired.installedHosts.includes(candidate);
|
|
1619
2161
|
const hostDrift = expected
|
|
1620
|
-
? status.kind !== "installed" ||
|
|
1621
|
-
desired.activeVersion === null ||
|
|
1622
|
-
status.version !== desired.activeVersion
|
|
2162
|
+
? status.kind !== "installed" || desired.activeVersion === null || status.version !== desired.activeVersion
|
|
1623
2163
|
: status.kind !== "missing";
|
|
1624
2164
|
drift ||= hostDrift;
|
|
1625
2165
|
if (status.kind === "installed") {
|
|
1626
2166
|
console.log(
|
|
1627
2167
|
`${candidate}: installed ${status.version ?? "unknown"}` +
|
|
2168
|
+
(["antigravity", "cursor"].includes(candidate)
|
|
2169
|
+
? " (experimental explicit-skill tier)"
|
|
2170
|
+
: candidate === "grok"
|
|
2171
|
+
? " (native skill; automatic and explicit invocation)"
|
|
2172
|
+
: candidate === "opencode"
|
|
2173
|
+
? " (stable same-turn bridge plus native skill fallback)"
|
|
2174
|
+
: "") +
|
|
1628
2175
|
(expected
|
|
1629
2176
|
? hostDrift
|
|
1630
2177
|
? ` (drift from ${desired.activeVersion ?? "desired state"})`
|
|
@@ -1644,9 +2191,7 @@ async function showStatus(host) {
|
|
|
1644
2191
|
} else if (status.kind === "files-only") {
|
|
1645
2192
|
console.log(`${candidate}: managed files present but not registered`);
|
|
1646
2193
|
} else {
|
|
1647
|
-
console.log(
|
|
1648
|
-
`${candidate}: not installed` + (expected ? " (drift: desired host is missing)" : ""),
|
|
1649
|
-
);
|
|
2194
|
+
console.log(`${candidate}: not installed` + (expected ? " (drift: desired host is missing)" : ""));
|
|
1650
2195
|
}
|
|
1651
2196
|
} catch (error) {
|
|
1652
2197
|
if (desired.installedHosts.includes(candidate)) drift = true;
|
|
@@ -1661,11 +2206,24 @@ async function showStatus(host) {
|
|
|
1661
2206
|
}
|
|
1662
2207
|
const status = await inspectHostStatus(host);
|
|
1663
2208
|
if (status.kind === "installed") {
|
|
1664
|
-
console.log(
|
|
2209
|
+
console.log(
|
|
2210
|
+
`OpenSocrates ${status.version ?? "unknown"} is installed.` +
|
|
2211
|
+
(host === "antigravity"
|
|
2212
|
+
? " Antigravity support is experimental and explicit-skill only."
|
|
2213
|
+
: host === "cursor"
|
|
2214
|
+
? " Cursor support is experimental and explicit-skill first."
|
|
2215
|
+
: host === "grok"
|
|
2216
|
+
? " Grok Build uses automatic native-skill selection and explicit /opensocrates invocation."
|
|
2217
|
+
: host === "opencode"
|
|
2218
|
+
? " OpenCode uses the stable same-turn bridge and native skill fallback."
|
|
2219
|
+
: ""),
|
|
2220
|
+
);
|
|
1665
2221
|
} else if (status.kind === "disabled") {
|
|
1666
2222
|
console.log(
|
|
1667
2223
|
`OpenSocrates ${status.version ?? "unknown"} is installed but disabled. ` +
|
|
1668
|
-
|
|
2224
|
+
(host === "grok"
|
|
2225
|
+
? "Remove the OpenSocrates disabled entry from Grok Build configuration, then run status again."
|
|
2226
|
+
: "Run install or update to re-enable it."),
|
|
1669
2227
|
);
|
|
1670
2228
|
} else if (status.kind === "available") {
|
|
1671
2229
|
console.log(`OpenSocrates ${status.version ?? "unknown"} is available but not installed.`);
|
|
@@ -1679,9 +2237,22 @@ async function showStatus(host) {
|
|
|
1679
2237
|
function removalTransaction(preflight) {
|
|
1680
2238
|
return {
|
|
1681
2239
|
...preflight,
|
|
1682
|
-
backup: join(
|
|
2240
|
+
backup: join(
|
|
2241
|
+
transientParent(preflight.host, preflight.paths),
|
|
2242
|
+
`.opensocrates.removed-${randomUUID()}`,
|
|
2243
|
+
),
|
|
2244
|
+
bridgeBackup:
|
|
2245
|
+
preflight.host === "opencode"
|
|
2246
|
+
? join(preflight.paths.bridgeParent, `.opensocrates.js.removed-${randomUUID()}`)
|
|
2247
|
+
: null,
|
|
2248
|
+
bridgeMarkerBackup:
|
|
2249
|
+
preflight.host === "opencode"
|
|
2250
|
+
? join(preflight.paths.bridgeParent, `.opensocrates-managed.json.removed-${randomUUID()}`)
|
|
2251
|
+
: null,
|
|
1683
2252
|
registrationRemoved: false,
|
|
1684
2253
|
backupCreated: false,
|
|
2254
|
+
bridgeBackupCreated: false,
|
|
2255
|
+
bridgeMarkerBackupCreated: false,
|
|
1685
2256
|
};
|
|
1686
2257
|
}
|
|
1687
2258
|
|
|
@@ -1695,6 +2266,14 @@ async function activateRemoval(transaction) {
|
|
|
1695
2266
|
await rename(paths.root, transaction.backup);
|
|
1696
2267
|
transaction.backupCreated = true;
|
|
1697
2268
|
}
|
|
2269
|
+
if (host === "opencode" && (await exists(paths.bridge))) {
|
|
2270
|
+
await rename(paths.bridge, transaction.bridgeBackup);
|
|
2271
|
+
transaction.bridgeBackupCreated = true;
|
|
2272
|
+
}
|
|
2273
|
+
if (host === "opencode" && (await exists(paths.bridgeMarker))) {
|
|
2274
|
+
await rename(paths.bridgeMarker, transaction.bridgeMarkerBackup);
|
|
2275
|
+
transaction.bridgeMarkerBackupCreated = true;
|
|
2276
|
+
}
|
|
1698
2277
|
}
|
|
1699
2278
|
|
|
1700
2279
|
async function rollbackRemoval(transaction) {
|
|
@@ -1715,6 +2294,18 @@ async function rollbackRemoval(transaction) {
|
|
|
1715
2294
|
);
|
|
1716
2295
|
});
|
|
1717
2296
|
}
|
|
2297
|
+
if (transaction.host === "opencode") {
|
|
2298
|
+
if (transaction.bridgeBackupCreated && (await exists(transaction.bridgeBackup))) {
|
|
2299
|
+
await recoveryStep("restore the removed OpenCode bridge", async () => {
|
|
2300
|
+
await rename(transaction.bridgeBackup, transaction.paths.bridge);
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
if (transaction.bridgeMarkerBackupCreated && (await exists(transaction.bridgeMarkerBackup))) {
|
|
2304
|
+
await recoveryStep("restore the removed OpenCode bridge sidecar", async () => {
|
|
2305
|
+
await rename(transaction.bridgeMarkerBackup, transaction.paths.bridgeMarker);
|
|
2306
|
+
});
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
1718
2309
|
}
|
|
1719
2310
|
|
|
1720
2311
|
async function commitRemoval(transaction) {
|
|
@@ -1722,6 +2313,19 @@ async function commitRemoval(transaction) {
|
|
|
1722
2313
|
await requireOwnedRoot(transaction.backup, transaction.host);
|
|
1723
2314
|
await rm(transaction.backup, { recursive: true });
|
|
1724
2315
|
}
|
|
2316
|
+
if (transaction.host === "opencode") {
|
|
2317
|
+
for (const [created, backup, label] of [
|
|
2318
|
+
[transaction.bridgeBackupCreated, transaction.bridgeBackup, "bridge"],
|
|
2319
|
+
[transaction.bridgeMarkerBackupCreated, transaction.bridgeMarkerBackup, "bridge sidecar"],
|
|
2320
|
+
]) {
|
|
2321
|
+
if (!created || !(await exists(backup))) continue;
|
|
2322
|
+
const info = await lstat(backup);
|
|
2323
|
+
if (!info.isFile() || info.isSymbolicLink()) {
|
|
2324
|
+
fail(`refusing to remove an unsafe OpenCode ${label} backup: ${backup}`);
|
|
2325
|
+
}
|
|
2326
|
+
await rm(backup);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
1725
2329
|
}
|
|
1726
2330
|
|
|
1727
2331
|
function xmlEscape(value) {
|
|
@@ -1762,29 +2366,15 @@ function launchctlDomain() {
|
|
|
1762
2366
|
|
|
1763
2367
|
function launchAgentDocument(npx, channel, environment) {
|
|
1764
2368
|
const packageTag = channel === "next" ? "next" : "latest";
|
|
1765
|
-
const arguments_ = [
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
`opensocrates@${packageTag}`,
|
|
1769
|
-
"auto-update",
|
|
1770
|
-
"run",
|
|
1771
|
-
];
|
|
1772
|
-
const argumentsXml = arguments_
|
|
1773
|
-
.map((argument) => ` <string>${xmlEscape(argument)}</string>`)
|
|
1774
|
-
.join("\n");
|
|
1775
|
-
const environmentEntries = Object.entries(environment).sort(([left], [right]) =>
|
|
1776
|
-
left.localeCompare(right),
|
|
1777
|
-
);
|
|
2369
|
+
const arguments_ = [npx, "--yes", `opensocrates@${packageTag}`, "auto-update", "run"];
|
|
2370
|
+
const argumentsXml = arguments_.map((argument) => ` <string>${xmlEscape(argument)}</string>`).join("\n");
|
|
2371
|
+
const environmentEntries = Object.entries(environment).sort(([left], [right]) => left.localeCompare(right));
|
|
1778
2372
|
const environmentXml =
|
|
1779
2373
|
environmentEntries.length === 0
|
|
1780
2374
|
? ""
|
|
1781
2375
|
: `\n <key>EnvironmentVariables</key>\n <dict>\n` +
|
|
1782
2376
|
environmentEntries
|
|
1783
|
-
.map(
|
|
1784
|
-
([key, value]) =>
|
|
1785
|
-
` <key>${xmlEscape(key)}</key>\n` +
|
|
1786
|
-
` <string>${xmlEscape(value)}</string>`,
|
|
1787
|
-
)
|
|
2377
|
+
.map(([key, value]) => ` <key>${xmlEscape(key)}</key>\n` + ` <string>${xmlEscape(value)}</string>`)
|
|
1788
2378
|
.join("\n") +
|
|
1789
2379
|
"\n </dict>";
|
|
1790
2380
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -1817,7 +2407,11 @@ function launchAgentTarget() {
|
|
|
1817
2407
|
}
|
|
1818
2408
|
|
|
1819
2409
|
function launchAgentLoaded() {
|
|
1820
|
-
return
|
|
2410
|
+
return (
|
|
2411
|
+
run(launchctlBinary(), ["print", launchAgentTarget()], {
|
|
2412
|
+
allowFailure: true,
|
|
2413
|
+
}).status === 0
|
|
2414
|
+
);
|
|
1821
2415
|
}
|
|
1822
2416
|
|
|
1823
2417
|
function stopLoadedLaunchAgent() {
|
|
@@ -1834,17 +2428,69 @@ function stopLoadedLaunchAgent() {
|
|
|
1834
2428
|
async function updaterEnvironment(hosts, npx) {
|
|
1835
2429
|
const node = await executablePath("node", "OPENSOCRATES_NODE_BIN");
|
|
1836
2430
|
const environment = {
|
|
1837
|
-
PATH: [...new Set([dirname(npx), dirname(node), "/usr/bin", "/bin", "/usr/sbin", "/sbin"])].join(
|
|
1838
|
-
":",
|
|
1839
|
-
),
|
|
2431
|
+
PATH: [...new Set([dirname(npx), dirname(node), "/usr/bin", "/bin", "/usr/sbin", "/sbin"])].join(":"),
|
|
1840
2432
|
};
|
|
1841
2433
|
for (const host of hosts) {
|
|
2434
|
+
if (host === "cursor") {
|
|
2435
|
+
if (process.env.CURSOR_CONFIG_DIR) {
|
|
2436
|
+
environment.CURSOR_CONFIG_DIR = resolve(process.env.CURSOR_CONFIG_DIR);
|
|
2437
|
+
continue;
|
|
2438
|
+
}
|
|
2439
|
+
const app = cursorAppPaths().find((candidate) => existsSync(candidate));
|
|
2440
|
+
if (app) continue;
|
|
2441
|
+
const executable = await executablePath("cursor", "CURSOR_BIN");
|
|
2442
|
+
environment.CURSOR_BIN = executable;
|
|
2443
|
+
environment.PATH = [...new Set([dirname(executable), ...environment.PATH.split(":")])].join(":");
|
|
2444
|
+
continue;
|
|
2445
|
+
}
|
|
2446
|
+
if (host === "antigravity") {
|
|
2447
|
+
if (process.env.ANTIGRAVITY_CONFIG_DIR) {
|
|
2448
|
+
environment.ANTIGRAVITY_CONFIG_DIR = resolve(process.env.ANTIGRAVITY_CONFIG_DIR);
|
|
2449
|
+
continue;
|
|
2450
|
+
}
|
|
2451
|
+
const executable = await executablePath("agy", "AGY_BIN");
|
|
2452
|
+
environment.AGY_BIN = executable;
|
|
2453
|
+
environment.PATH = [...new Set([dirname(executable), ...environment.PATH.split(":")])].join(":");
|
|
2454
|
+
continue;
|
|
2455
|
+
}
|
|
2456
|
+
if (host === "opencode") {
|
|
2457
|
+
// OPENCODE_CONFIG_DIR redirects configuration only. Unlike the
|
|
2458
|
+
// content-only hosts above, a scheduled OpenCode update re-runs the same
|
|
2459
|
+
// requireHostCli version gate as an interactive install, so recording
|
|
2460
|
+
// the config directory and stopping would leave the LaunchAgent without
|
|
2461
|
+
// a resolvable opencode: enable would succeed in a shell that has it on
|
|
2462
|
+
// PATH, then every scheduled run would fail the gate whenever opencode
|
|
2463
|
+
// lives outside the launchd default PATH.
|
|
2464
|
+
if (process.env.OPENCODE_CONFIG_DIR) {
|
|
2465
|
+
environment.OPENCODE_CONFIG_DIR = resolve(process.env.OPENCODE_CONFIG_DIR);
|
|
2466
|
+
}
|
|
2467
|
+
const executable = await executablePath("opencode", "OPENCODE_BIN");
|
|
2468
|
+
environment.OPENCODE_BIN = executable;
|
|
2469
|
+
environment.PATH = [...new Set([dirname(executable), ...environment.PATH.split(":")])].join(":");
|
|
2470
|
+
continue;
|
|
2471
|
+
}
|
|
2472
|
+
if (host === "grok") {
|
|
2473
|
+
const executable = await executablePath("grok", "GROK_BIN");
|
|
2474
|
+
environment.GROK_BIN = executable;
|
|
2475
|
+
environment.PATH = [...new Set([dirname(executable), ...environment.PATH.split(":")])].join(
|
|
2476
|
+
":",
|
|
2477
|
+
);
|
|
2478
|
+
continue;
|
|
2479
|
+
}
|
|
1842
2480
|
const key = host === "claude" ? "CLAUDE_BIN" : "CODEX_BIN";
|
|
1843
2481
|
const executable = await executablePath(host, key);
|
|
1844
2482
|
environment[key] = executable;
|
|
1845
2483
|
environment.PATH = [...new Set([dirname(executable), ...environment.PATH.split(":")])].join(":");
|
|
1846
2484
|
}
|
|
1847
|
-
for (const key of [
|
|
2485
|
+
for (const key of [
|
|
2486
|
+
"ANTIGRAVITY_CONFIG_DIR",
|
|
2487
|
+
"CLAUDE_CONFIG_DIR",
|
|
2488
|
+
"CODEX_HOME",
|
|
2489
|
+
"CURSOR_CONFIG_DIR",
|
|
2490
|
+
"GROK_HOME",
|
|
2491
|
+
"OPENCODE_CONFIG_DIR",
|
|
2492
|
+
"OPENSOCRATES_STATE_DIR",
|
|
2493
|
+
]) {
|
|
1848
2494
|
if (process.env[key]) environment[key] = resolve(process.env[key]);
|
|
1849
2495
|
}
|
|
1850
2496
|
return environment;
|
|
@@ -1852,10 +2498,7 @@ async function updaterEnvironment(hosts, npx) {
|
|
|
1852
2498
|
|
|
1853
2499
|
async function installLaunchAgent(channel, hosts) {
|
|
1854
2500
|
if (process.platform !== "darwin" || process.arch !== "arm64") {
|
|
1855
|
-
fail(
|
|
1856
|
-
`automatic updates currently support darwin-arm64 only; detected ` +
|
|
1857
|
-
`${process.platform}-${process.arch}`,
|
|
1858
|
-
);
|
|
2501
|
+
fail(`automatic updates currently support darwin-arm64 only; detected ` + `${process.platform}-${process.arch}`);
|
|
1859
2502
|
}
|
|
1860
2503
|
const paths = statePaths();
|
|
1861
2504
|
const npx = await executablePath("npx", "OPENSOCRATES_NPX_BIN");
|
|
@@ -1876,22 +2519,16 @@ async function installLaunchAgent(channel, hosts) {
|
|
|
1876
2519
|
const wasLoaded = launchAgentLoaded();
|
|
1877
2520
|
if (wasLoaded) stopLoadedLaunchAgent();
|
|
1878
2521
|
await atomicWritePrivateFile(paths.launchAgent, document);
|
|
1879
|
-
const launched = run(
|
|
1880
|
-
launchctlBinary(),
|
|
1881
|
-
["bootstrap", launchctlDomain(), paths.launchAgent],
|
|
1882
|
-
{ allowFailure: true },
|
|
1883
|
-
);
|
|
2522
|
+
const launched = run(launchctlBinary(), ["bootstrap", launchctlDomain(), paths.launchAgent], { allowFailure: true });
|
|
1884
2523
|
if (launched.status !== 0) {
|
|
1885
2524
|
if (previousDocument === null) {
|
|
1886
2525
|
await rm(paths.launchAgent, { force: true });
|
|
1887
2526
|
} else {
|
|
1888
2527
|
await atomicWritePrivateFile(paths.launchAgent, previousDocument);
|
|
1889
2528
|
if (wasLoaded) {
|
|
1890
|
-
const restored = run(
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
{ allowFailure: true },
|
|
1894
|
-
);
|
|
2529
|
+
const restored = run(launchctlBinary(), ["bootstrap", launchctlDomain(), paths.launchAgent], {
|
|
2530
|
+
allowFailure: true,
|
|
2531
|
+
});
|
|
1895
2532
|
if (restored.status !== 0) {
|
|
1896
2533
|
console.error("warning: the previous OpenSocrates LaunchAgent could not be reloaded");
|
|
1897
2534
|
}
|
|
@@ -1930,9 +2567,7 @@ async function runRemove(options) {
|
|
|
1930
2567
|
const preflights = await preflightSelectedHosts(options, "remove", desired);
|
|
1931
2568
|
const removedHosts = preflights.map((item) => item.host);
|
|
1932
2569
|
const remainingHosts = desired.installedHosts.filter((host) => !removedHosts.includes(host));
|
|
1933
|
-
const remainingAutoUpdateHosts = desired.autoUpdate.hosts.filter(
|
|
1934
|
-
(host) => !removedHosts.includes(host),
|
|
1935
|
-
);
|
|
2570
|
+
const remainingAutoUpdateHosts = desired.autoUpdate.hosts.filter((host) => !removedHosts.includes(host));
|
|
1936
2571
|
const keepAutoUpdate = desired.autoUpdate.enabled && remainingAutoUpdateHosts.length > 0;
|
|
1937
2572
|
const nextDesired = {
|
|
1938
2573
|
...desired,
|
|
@@ -1964,21 +2599,22 @@ async function runRemove(options) {
|
|
|
1964
2599
|
} catch (error) {
|
|
1965
2600
|
for (const transaction of [...transactions].reverse()) await rollbackRemoval(transaction);
|
|
1966
2601
|
if (schedulerTouched) {
|
|
1967
|
-
const schedulerRestored = await recoveryStep(
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
}
|
|
1975
|
-
},
|
|
1976
|
-
);
|
|
2602
|
+
const schedulerRestored = await recoveryStep("restore the automatic updater after removal failure", async () => {
|
|
2603
|
+
if (desired.autoUpdate.enabled) {
|
|
2604
|
+
await installLaunchAgent(desired.channel, desired.autoUpdate.hosts);
|
|
2605
|
+
} else {
|
|
2606
|
+
await disableLaunchAgent();
|
|
2607
|
+
}
|
|
2608
|
+
});
|
|
1977
2609
|
if (!schedulerRestored && desired.autoUpdate.enabled) {
|
|
1978
2610
|
await recoveryStep("record the disabled updater after removal failure", async () => {
|
|
1979
2611
|
await writeDesiredState({
|
|
1980
2612
|
...desired,
|
|
1981
|
-
autoUpdate: {
|
|
2613
|
+
autoUpdate: {
|
|
2614
|
+
enabled: false,
|
|
2615
|
+
hosts: [],
|
|
2616
|
+
nextCheckAt: null,
|
|
2617
|
+
},
|
|
1982
2618
|
});
|
|
1983
2619
|
});
|
|
1984
2620
|
}
|
|
@@ -1986,7 +2622,11 @@ async function runRemove(options) {
|
|
|
1986
2622
|
await recoveryStep("record the disabled updater after removal failure", async () => {
|
|
1987
2623
|
await writeDesiredState({
|
|
1988
2624
|
...desired,
|
|
1989
|
-
autoUpdate: {
|
|
2625
|
+
autoUpdate: {
|
|
2626
|
+
enabled: false,
|
|
2627
|
+
hosts: [],
|
|
2628
|
+
nextCheckAt: null,
|
|
2629
|
+
},
|
|
1990
2630
|
});
|
|
1991
2631
|
});
|
|
1992
2632
|
}
|
|
@@ -1994,7 +2634,19 @@ async function runRemove(options) {
|
|
|
1994
2634
|
}
|
|
1995
2635
|
for (const transaction of transactions) await commitRemoval(transaction);
|
|
1996
2636
|
for (const host of removedHosts) {
|
|
1997
|
-
|
|
2637
|
+
const label =
|
|
2638
|
+
host === "antigravity"
|
|
2639
|
+
? "Antigravity"
|
|
2640
|
+
: host === "claude"
|
|
2641
|
+
? "Claude"
|
|
2642
|
+
: host === "cursor"
|
|
2643
|
+
? "Cursor"
|
|
2644
|
+
: host === "grok"
|
|
2645
|
+
? "Grok Build"
|
|
2646
|
+
: host === "opencode"
|
|
2647
|
+
? "OpenCode"
|
|
2648
|
+
: "Codex";
|
|
2649
|
+
console.log(`OpenSocrates was removed from ${label}.`);
|
|
1998
2650
|
}
|
|
1999
2651
|
if (removedHosts.length === 0) console.log("OpenSocrates is not installed on any managed host.");
|
|
2000
2652
|
}
|
|
@@ -2035,19 +2687,14 @@ async function enableAutoUpdate(options) {
|
|
|
2035
2687
|
const observedVersions = new Set();
|
|
2036
2688
|
for (const host of installedHosts) {
|
|
2037
2689
|
const preflight = preflights.get(host) ?? (await preflightHost(host, "update"));
|
|
2038
|
-
if (
|
|
2039
|
-
preflight.previousState.kind !== "installed" ||
|
|
2040
|
-
typeof preflight.previousState.version !== "string"
|
|
2041
|
-
) {
|
|
2690
|
+
if (preflight.previousState.kind !== "installed" || typeof preflight.previousState.version !== "string") {
|
|
2042
2691
|
fail(`cannot determine the installed OpenSocrates version on ${host}`);
|
|
2043
2692
|
}
|
|
2044
2693
|
preflights.set(host, preflight);
|
|
2045
2694
|
observedVersions.add(preflight.previousState.version);
|
|
2046
2695
|
}
|
|
2047
2696
|
if (observedVersions.size !== 1) {
|
|
2048
|
-
fail(
|
|
2049
|
-
"installed hosts do not share one known version; run update --host all before enabling automatic updates",
|
|
2050
|
-
);
|
|
2697
|
+
fail("installed hosts do not share one known version; run update --host all before enabling automatic updates");
|
|
2051
2698
|
}
|
|
2052
2699
|
[activeVersion] = observedVersions;
|
|
2053
2700
|
}
|
|
@@ -2151,18 +2798,20 @@ async function runScheduledUpdate(options) {
|
|
|
2151
2798
|
for (const host of hosts) ready.push(await preflightHost(host, "update"));
|
|
2152
2799
|
const alreadyCurrent =
|
|
2153
2800
|
desired.activeVersion === PRODUCT_VERSION &&
|
|
2154
|
-
ready.every(
|
|
2155
|
-
(item) =>
|
|
2156
|
-
item.previousState.kind === "installed" && item.previousState.version === PRODUCT_VERSION,
|
|
2157
|
-
);
|
|
2801
|
+
ready.every((item) => item.previousState.kind === "installed" && item.previousState.version === PRODUCT_VERSION);
|
|
2158
2802
|
if (!alreadyCurrent) {
|
|
2159
|
-
if (hosts.length
|
|
2803
|
+
if (hosts.length > 1) {
|
|
2160
2804
|
await runInstallOrUpdate({ ...options, host: ALL_HOST }, "update");
|
|
2161
2805
|
} else {
|
|
2162
2806
|
const [host] = hosts;
|
|
2163
2807
|
const local = localAssetInputs(options, host);
|
|
2164
2808
|
await runInstallOrUpdate(
|
|
2165
|
-
{
|
|
2809
|
+
{
|
|
2810
|
+
...options,
|
|
2811
|
+
host,
|
|
2812
|
+
asset: local.asset,
|
|
2813
|
+
checksum: local.checksum,
|
|
2814
|
+
},
|
|
2166
2815
|
"update",
|
|
2167
2816
|
);
|
|
2168
2817
|
}
|
|
@@ -2172,15 +2821,16 @@ async function runScheduledUpdate(options) {
|
|
|
2172
2821
|
...refreshed,
|
|
2173
2822
|
availableVersion: PRODUCT_VERSION,
|
|
2174
2823
|
lastCheckAt: checkedAt,
|
|
2175
|
-
lastSuccessfulUpdateAt: alreadyCurrent
|
|
2176
|
-
? refreshed.lastSuccessfulUpdateAt
|
|
2177
|
-
: checkedAt,
|
|
2824
|
+
lastSuccessfulUpdateAt: alreadyCurrent ? refreshed.lastSuccessfulUpdateAt : checkedAt,
|
|
2178
2825
|
autoUpdate: { ...refreshed.autoUpdate, nextCheckAt: next },
|
|
2179
2826
|
});
|
|
2180
2827
|
await writeAutoUpdateReceipt({
|
|
2181
2828
|
version: PRODUCT_VERSION,
|
|
2182
2829
|
checkedAt,
|
|
2183
|
-
hosts: hosts.map((host) => ({
|
|
2830
|
+
hosts: hosts.map((host) => ({
|
|
2831
|
+
host,
|
|
2832
|
+
result: alreadyCurrent ? "current" : "updated",
|
|
2833
|
+
})),
|
|
2184
2834
|
result: alreadyCurrent ? "no-update" : "updated",
|
|
2185
2835
|
errorCategory: null,
|
|
2186
2836
|
});
|
|
@@ -2219,19 +2869,31 @@ function requireSupportedPlatform() {
|
|
|
2219
2869
|
}
|
|
2220
2870
|
|
|
2221
2871
|
async function verifyPackages(options) {
|
|
2222
|
-
const
|
|
2872
|
+
const assetHosts = qualifiedAssetHosts(options);
|
|
2873
|
+
const hosts =
|
|
2874
|
+
options.host === ALL_HOST ? (assetHosts.length > 0 ? assetHosts : SUPPORTED_HOSTS) : [options.host];
|
|
2223
2875
|
const prepared = await prepareVerifiedPackages(options, hosts);
|
|
2224
2876
|
try {
|
|
2225
2877
|
for (const item of prepared) {
|
|
2226
2878
|
console.log(
|
|
2227
|
-
`${item.host}: verified OpenSocrates ${PRODUCT_VERSION} release and ` +
|
|
2228
|
-
`${item.checkedFiles} package files.`,
|
|
2879
|
+
`${item.host}: verified OpenSocrates ${PRODUCT_VERSION} release and ` + `${item.checkedFiles} package files.`,
|
|
2229
2880
|
);
|
|
2881
|
+
if (item.host === "opencode") {
|
|
2882
|
+
const paths = managedPaths("opencode");
|
|
2883
|
+
const presence = await Promise.all(
|
|
2884
|
+
[paths.root, paths.bridge, paths.bridgeMarker].map((target) => exists(target)),
|
|
2885
|
+
);
|
|
2886
|
+
if (presence.some(Boolean)) {
|
|
2887
|
+
if (!presence.every(Boolean)) {
|
|
2888
|
+
fail("OpenCode installed state is partial and failed verification");
|
|
2889
|
+
}
|
|
2890
|
+
await verifyOpenCodeInstallation(paths);
|
|
2891
|
+
console.log("opencode: verified installed bridge, skill inventory, and ownership.");
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2230
2894
|
}
|
|
2231
2895
|
} finally {
|
|
2232
|
-
await Promise.all(
|
|
2233
|
-
prepared.map((item) => rm(item.scratch, { recursive: true, force: true })),
|
|
2234
|
-
);
|
|
2896
|
+
await Promise.all(prepared.map((item) => rm(item.scratch, { recursive: true, force: true })));
|
|
2235
2897
|
}
|
|
2236
2898
|
}
|
|
2237
2899
|
|