opensocrates 1.0.0 → 1.1.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 +54 -0
- package/README.ko.md +140 -77
- package/README.md +146 -84
- package/SECURITY.md +38 -5
- package/VERSION +1 -1
- package/installer/opensocrates.mjs +387 -123
- package/package.json +6 -3
|
@@ -22,22 +22,46 @@ import { Readable } from "node:stream";
|
|
|
22
22
|
import { pipeline } from "node:stream/promises";
|
|
23
23
|
import { fileURLToPath } from "node:url";
|
|
24
24
|
|
|
25
|
-
export const PRODUCT_VERSION = "1.
|
|
25
|
+
export const PRODUCT_VERSION = "1.1.0";
|
|
26
26
|
export const REPOSITORY = "ParkerHwang/OpenSocrates";
|
|
27
27
|
export const MARKETPLACE_NAME = "opensocrates";
|
|
28
28
|
export const PLUGIN_NAME = "opensocrates";
|
|
29
29
|
export const PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
|
|
30
|
-
export const
|
|
30
|
+
export const DEFAULT_HOST = "codex";
|
|
31
|
+
export const SUPPORTED_HOSTS = Object.freeze(["claude", "codex"]);
|
|
32
|
+
export function assetNameFor(host = DEFAULT_HOST) {
|
|
33
|
+
if (!SUPPORTED_HOSTS.includes(host)) {
|
|
34
|
+
fail(`unsupported host ${JSON.stringify(host)}`);
|
|
35
|
+
}
|
|
36
|
+
return `opensocrates-${PRODUCT_VERSION}-${host}-plugin.zip`;
|
|
37
|
+
}
|
|
38
|
+
export const ASSET_NAME = assetNameFor(DEFAULT_HOST);
|
|
31
39
|
export const CHECKSUM_NAME = `${ASSET_NAME}.sha256`;
|
|
32
40
|
|
|
33
41
|
const MARKER_NAME = ".opensocrates-managed.json";
|
|
34
|
-
const
|
|
42
|
+
const CODEX_MARKER = Object.freeze({
|
|
43
|
+
schemaVersion: 1,
|
|
44
|
+
marketplaceName: MARKETPLACE_NAME,
|
|
45
|
+
pluginName: PLUGIN_NAME,
|
|
46
|
+
});
|
|
47
|
+
const CLAUDE_MARKER = Object.freeze({
|
|
35
48
|
schemaVersion: 1,
|
|
36
49
|
marketplaceName: MARKETPLACE_NAME,
|
|
37
50
|
pluginName: PLUGIN_NAME,
|
|
51
|
+
host: "claude",
|
|
52
|
+
});
|
|
53
|
+
const HOST_LAYOUTS = Object.freeze({
|
|
54
|
+
claude: {
|
|
55
|
+
marketplaceRelative: join(".claude-plugin", "marketplace.json"),
|
|
56
|
+
pluginRelative: join("plugins", PLUGIN_NAME),
|
|
57
|
+
manifestRelative: join(".claude-plugin", "plugin.json"),
|
|
58
|
+
},
|
|
59
|
+
codex: {
|
|
60
|
+
marketplaceRelative: join(".agents", "plugins", "marketplace.json"),
|
|
61
|
+
pluginRelative: join("build", "generated", "plugins", "codex"),
|
|
62
|
+
manifestRelative: join(".codex-plugin", "plugin.json"),
|
|
63
|
+
},
|
|
38
64
|
});
|
|
39
|
-
const MARKETPLACE_RELATIVE = join(".agents", "plugins", "marketplace.json");
|
|
40
|
-
const PLUGIN_RELATIVE = join("build", "generated", "plugins", "codex");
|
|
41
65
|
const MAX_DOWNLOAD_BYTES = 2 * 1024 * 1024 * 1024;
|
|
42
66
|
const MAX_ARCHIVE_ENTRIES = 10_000;
|
|
43
67
|
|
|
@@ -65,12 +89,16 @@ function jsonEqual(left, right) {
|
|
|
65
89
|
);
|
|
66
90
|
}
|
|
67
91
|
|
|
68
|
-
|
|
92
|
+
function markerFor(host) {
|
|
93
|
+
return host === "claude" ? CLAUDE_MARKER : CODEX_MARKER;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function markerMatches(value, host = DEFAULT_HOST) {
|
|
69
97
|
return (
|
|
70
98
|
value !== null &&
|
|
71
99
|
typeof value === "object" &&
|
|
72
100
|
!Array.isArray(value) &&
|
|
73
|
-
jsonEqual(value,
|
|
101
|
+
jsonEqual(value, markerFor(host))
|
|
74
102
|
);
|
|
75
103
|
}
|
|
76
104
|
|
|
@@ -120,13 +148,21 @@ export function parseCli(argv) {
|
|
|
120
148
|
if (!actions.has(action)) {
|
|
121
149
|
fail(`unknown action ${JSON.stringify(action)}`);
|
|
122
150
|
}
|
|
123
|
-
const options = { action, asset: null, checksum: null };
|
|
151
|
+
const options = { action, host: DEFAULT_HOST, asset: null, checksum: null };
|
|
124
152
|
while (args.length > 0) {
|
|
125
153
|
const flag = args.shift();
|
|
126
154
|
if (flag === "--help" || flag === "-h") {
|
|
127
155
|
options.action = "help";
|
|
128
156
|
continue;
|
|
129
157
|
}
|
|
158
|
+
if (flag === "--host") {
|
|
159
|
+
const host = args.shift();
|
|
160
|
+
if (!SUPPORTED_HOSTS.includes(host)) {
|
|
161
|
+
fail(`--host must be one of: ${SUPPORTED_HOSTS.join(", ")}`);
|
|
162
|
+
}
|
|
163
|
+
options.host = host;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
130
166
|
if (flag !== "--asset" && flag !== "--checksum") {
|
|
131
167
|
fail(`unknown option ${JSON.stringify(flag)}`);
|
|
132
168
|
}
|
|
@@ -149,28 +185,40 @@ function showHelp() {
|
|
|
149
185
|
console.log(`OpenSocrates ${PRODUCT_VERSION}
|
|
150
186
|
|
|
151
187
|
Usage:
|
|
152
|
-
opensocrates install [--asset ZIP --checksum SHA256]
|
|
153
|
-
opensocrates status
|
|
154
|
-
opensocrates update [--asset ZIP --checksum SHA256]
|
|
155
|
-
opensocrates remove
|
|
156
|
-
opensocrates verify [--asset ZIP --checksum SHA256]
|
|
188
|
+
opensocrates install [--host codex|claude] [--asset ZIP --checksum SHA256]
|
|
189
|
+
opensocrates status [--host codex|claude]
|
|
190
|
+
opensocrates update [--host codex|claude] [--asset ZIP --checksum SHA256]
|
|
191
|
+
opensocrates remove [--host codex|claude]
|
|
192
|
+
opensocrates verify [--host codex|claude] [--asset ZIP --checksum SHA256]
|
|
157
193
|
|
|
158
194
|
Without --asset, install, update, and verify download the v${PRODUCT_VERSION}
|
|
159
|
-
package and checksum from GitHub Releases.
|
|
195
|
+
package and checksum from GitHub Releases. The default host is codex.
|
|
160
196
|
`);
|
|
161
197
|
}
|
|
162
198
|
|
|
163
|
-
function managedPaths() {
|
|
164
|
-
const configured =
|
|
165
|
-
|
|
166
|
-
const
|
|
199
|
+
function managedPaths(host) {
|
|
200
|
+
const configured =
|
|
201
|
+
host === "claude" ? process.env.CLAUDE_CONFIG_DIR : process.env.CODEX_HOME;
|
|
202
|
+
const configuredHome = resolve(
|
|
203
|
+
configured ? configured : join(homedir(), host === "claude" ? ".claude" : ".codex"),
|
|
204
|
+
);
|
|
205
|
+
let hostHome = configuredHome;
|
|
206
|
+
try {
|
|
207
|
+
hostHome = realpathSync(configuredHome);
|
|
208
|
+
} catch {
|
|
209
|
+
// The installer creates a missing host home below after validating its
|
|
210
|
+
// explicit, non-recursive target. Lexical resolution is the only
|
|
211
|
+
// available normalization until then.
|
|
212
|
+
}
|
|
213
|
+
const root = join(hostHome, "managed-marketplaces", MARKETPLACE_NAME);
|
|
214
|
+
const layout = HOST_LAYOUTS[host];
|
|
167
215
|
return {
|
|
168
|
-
|
|
216
|
+
hostHome,
|
|
169
217
|
root,
|
|
170
218
|
parent: dirname(root),
|
|
171
219
|
marker: join(root, MARKER_NAME),
|
|
172
|
-
marketplace: join(root,
|
|
173
|
-
plugin: join(root,
|
|
220
|
+
marketplace: join(root, layout.marketplaceRelative),
|
|
221
|
+
plugin: join(root, layout.pluginRelative),
|
|
174
222
|
};
|
|
175
223
|
}
|
|
176
224
|
|
|
@@ -178,6 +226,10 @@ function codexBinary() {
|
|
|
178
226
|
return process.env.CODEX_BIN || "codex";
|
|
179
227
|
}
|
|
180
228
|
|
|
229
|
+
function claudeBinary() {
|
|
230
|
+
return process.env.CLAUDE_BIN || "claude";
|
|
231
|
+
}
|
|
232
|
+
|
|
181
233
|
function run(command, args, { allowFailure = false } = {}) {
|
|
182
234
|
const result = spawnSync(command, args, {
|
|
183
235
|
encoding: "utf8",
|
|
@@ -197,47 +249,97 @@ function run(command, args, { allowFailure = false } = {}) {
|
|
|
197
249
|
return result;
|
|
198
250
|
}
|
|
199
251
|
|
|
200
|
-
function
|
|
201
|
-
const result = run(
|
|
252
|
+
function runJson(command, args, label) {
|
|
253
|
+
const result = run(command, args);
|
|
202
254
|
try {
|
|
203
255
|
const payload = JSON.parse(result.stdout);
|
|
204
|
-
if (payload === null || typeof payload !== "object"
|
|
205
|
-
fail(
|
|
256
|
+
if (payload === null || typeof payload !== "object") {
|
|
257
|
+
fail(`${label} returned a non-container JSON value`);
|
|
206
258
|
}
|
|
207
259
|
return payload;
|
|
208
260
|
} catch (error) {
|
|
209
261
|
if (error instanceof InstallerError) {
|
|
210
262
|
throw error;
|
|
211
263
|
}
|
|
212
|
-
fail(
|
|
264
|
+
fail(`${label} returned invalid JSON for ${args.join(" ")}: ${error.message}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function runCodexJson(args) {
|
|
269
|
+
const payload = runJson(codexBinary(), args, "Codex");
|
|
270
|
+
if (Array.isArray(payload)) {
|
|
271
|
+
fail("Codex returned an unexpected JSON array");
|
|
213
272
|
}
|
|
273
|
+
return payload;
|
|
214
274
|
}
|
|
215
275
|
|
|
216
|
-
function
|
|
276
|
+
function runClaudeJson(args) {
|
|
277
|
+
return runJson(claudeBinary(), args, "Claude Code");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function versionAtLeast(value, minimum) {
|
|
281
|
+
const match = String(value).match(/(\d+)\.(\d+)\.(\d+)/u);
|
|
282
|
+
if (!match) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
const current = match.slice(1).map(Number);
|
|
286
|
+
for (let index = 0; index < minimum.length; index += 1) {
|
|
287
|
+
if (current[index] > minimum[index]) return true;
|
|
288
|
+
if (current[index] < minimum[index]) return false;
|
|
289
|
+
}
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function requireHost(host) {
|
|
294
|
+
if (host === "claude") {
|
|
295
|
+
const result = run(claudeBinary(), ["--version"]);
|
|
296
|
+
if (!versionAtLeast(result.stdout, [2, 1, 205])) {
|
|
297
|
+
fail(
|
|
298
|
+
`Claude Code 2.1.205 or later is required for structured selector output; got ${result.stdout.trim()}`,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
217
303
|
run(codexBinary(), ["--version"]);
|
|
218
304
|
}
|
|
219
305
|
|
|
220
|
-
function
|
|
221
|
-
const payload =
|
|
222
|
-
|
|
306
|
+
function marketplaceEntries(host) {
|
|
307
|
+
const payload =
|
|
308
|
+
host === "claude"
|
|
309
|
+
? runClaudeJson(["plugin", "marketplace", "list", "--json"])
|
|
310
|
+
: runCodexJson(["plugin", "marketplace", "list", "--json"]).marketplaces;
|
|
311
|
+
const entries = payload;
|
|
223
312
|
if (!Array.isArray(entries)) {
|
|
224
|
-
fail(
|
|
313
|
+
fail(`${host} marketplace list returned an unexpected schema`);
|
|
225
314
|
}
|
|
315
|
+
return entries;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function marketplaceEntry(host) {
|
|
319
|
+
const entries = marketplaceEntries(host);
|
|
226
320
|
const matches = entries.filter((entry) => entry?.name === MARKETPLACE_NAME);
|
|
227
321
|
if (matches.length > 1) {
|
|
228
|
-
fail(
|
|
322
|
+
fail(`${host} reported duplicate marketplaces named ${MARKETPLACE_NAME}`);
|
|
229
323
|
}
|
|
230
324
|
return matches[0] ?? null;
|
|
231
325
|
}
|
|
232
326
|
|
|
233
|
-
function pluginState() {
|
|
327
|
+
function pluginState(host) {
|
|
328
|
+
if (host === "claude") {
|
|
329
|
+
const installed = runClaudeJson(["plugin", "list", "--json"]);
|
|
330
|
+
if (!Array.isArray(installed)) {
|
|
331
|
+
fail("Claude Code plugin list returned an unexpected schema");
|
|
332
|
+
}
|
|
333
|
+
const matches = installed.filter((entry) => entry?.id === PLUGIN_ID);
|
|
334
|
+
if (matches.length > 1) {
|
|
335
|
+
fail(`Claude Code reported duplicate entries for ${PLUGIN_ID}`);
|
|
336
|
+
}
|
|
337
|
+
return matches.length === 1
|
|
338
|
+
? { kind: "installed", version: matches[0].version ?? null }
|
|
339
|
+
: { kind: "missing", version: null };
|
|
340
|
+
}
|
|
234
341
|
const payload = runCodexJson([
|
|
235
|
-
"plugin",
|
|
236
|
-
"list",
|
|
237
|
-
"--marketplace",
|
|
238
|
-
MARKETPLACE_NAME,
|
|
239
|
-
"--available",
|
|
240
|
-
"--json",
|
|
342
|
+
"plugin", "list", "--marketplace", MARKETPLACE_NAME, "--available", "--json",
|
|
241
343
|
]);
|
|
242
344
|
const installed = Array.isArray(payload.installed) ? payload.installed : null;
|
|
243
345
|
const available = Array.isArray(payload.available) ? payload.available : null;
|
|
@@ -258,6 +360,56 @@ function pluginState() {
|
|
|
258
360
|
return { kind: "missing", version: null };
|
|
259
361
|
}
|
|
260
362
|
|
|
363
|
+
function detectLegacyClaudeInstallation() {
|
|
364
|
+
const marketplaces = marketplaceEntries("claude").filter(
|
|
365
|
+
(entry) =>
|
|
366
|
+
typeof entry?.name === "string" &&
|
|
367
|
+
entry.name !== MARKETPLACE_NAME &&
|
|
368
|
+
entry.name.toLowerCase() === MARKETPLACE_NAME,
|
|
369
|
+
);
|
|
370
|
+
const plugins = runClaudeJson(["plugin", "list", "--json"]);
|
|
371
|
+
const legacyPlugins = Array.isArray(plugins)
|
|
372
|
+
? plugins.filter(
|
|
373
|
+
(entry) =>
|
|
374
|
+
typeof entry?.id === "string" &&
|
|
375
|
+
entry.id !== PLUGIN_ID &&
|
|
376
|
+
entry.id.toLowerCase() === PLUGIN_ID,
|
|
377
|
+
)
|
|
378
|
+
: [];
|
|
379
|
+
const names = marketplaces.map((entry) => entry.name).join(", ") || "OpenSocrates";
|
|
380
|
+
return { found: marketplaces.length > 0 || legacyPlugins.length > 0, names };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Install and update write into the managed root, so a case-variant pre-1.0
|
|
384
|
+
// registration must be resolved by the user first. Status and remove are
|
|
385
|
+
// diagnostic and scoped to the root this installer owns, so they only warn:
|
|
386
|
+
// blocking them would strand a user who needs to inspect or back out.
|
|
387
|
+
function requireNoLegacyClaudeInstallation() {
|
|
388
|
+
const legacy = detectLegacyClaudeInstallation();
|
|
389
|
+
if (legacy.found) {
|
|
390
|
+
fail(
|
|
391
|
+
`a legacy Claude marketplace (${legacy.names}) is installed; OpenSocrates will not remove it automatically. ` +
|
|
392
|
+
"Uninstall its plugin and marketplace explicitly, then rerun this command. See the README migration section.",
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function warnLegacyClaudeInstallation() {
|
|
398
|
+
let legacy;
|
|
399
|
+
try {
|
|
400
|
+
legacy = detectLegacyClaudeInstallation();
|
|
401
|
+
} catch {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (legacy.found) {
|
|
405
|
+
console.warn(
|
|
406
|
+
`warning: a legacy Claude marketplace (${legacy.names}) is also registered. ` +
|
|
407
|
+
"OpenSocrates never removes it automatically; this command only affects the root it owns. " +
|
|
408
|
+
"See the README migration section.",
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
261
413
|
async function readJsonObject(target) {
|
|
262
414
|
let payload;
|
|
263
415
|
try {
|
|
@@ -271,13 +423,13 @@ async function readJsonObject(target) {
|
|
|
271
423
|
return payload;
|
|
272
424
|
}
|
|
273
425
|
|
|
274
|
-
async function requireOwnedRoot(root) {
|
|
426
|
+
async function requireOwnedRoot(root, host) {
|
|
275
427
|
const info = await lstat(root);
|
|
276
428
|
if (!info.isDirectory() || info.isSymbolicLink()) {
|
|
277
429
|
fail(`managed marketplace path is not an owned directory: ${root}`);
|
|
278
430
|
}
|
|
279
431
|
const marker = await readJsonObject(join(root, MARKER_NAME));
|
|
280
|
-
if (!markerMatches(marker)) {
|
|
432
|
+
if (!markerMatches(marker, host)) {
|
|
281
433
|
fail(`managed marketplace path has no valid ownership marker: ${root}`);
|
|
282
434
|
}
|
|
283
435
|
}
|
|
@@ -321,16 +473,18 @@ async function resolveAssetInputs(options, scratch) {
|
|
|
321
473
|
return { asset: options.asset, checksum: options.checksum };
|
|
322
474
|
}
|
|
323
475
|
const base = `https://github.com/${REPOSITORY}/releases/download/v${PRODUCT_VERSION}`;
|
|
324
|
-
const
|
|
325
|
-
const
|
|
476
|
+
const assetName = assetNameFor(options.host);
|
|
477
|
+
const checksumName = `${assetName}.sha256`;
|
|
478
|
+
const asset = join(scratch, assetName);
|
|
479
|
+
const checksum = join(scratch, checksumName);
|
|
326
480
|
console.log(`Downloading OpenSocrates ${PRODUCT_VERSION} from GitHub Releases...`);
|
|
327
|
-
await downloadFile(`${base}/${
|
|
328
|
-
await downloadFile(`${base}/${
|
|
481
|
+
await downloadFile(`${base}/${assetName}`, asset);
|
|
482
|
+
await downloadFile(`${base}/${checksumName}`, checksum);
|
|
329
483
|
return { asset, checksum };
|
|
330
484
|
}
|
|
331
485
|
|
|
332
|
-
async function verifyOuterChecksum(asset, checksum) {
|
|
333
|
-
const expected = parseChecksumText(await readFile(checksum, "utf8"));
|
|
486
|
+
async function verifyOuterChecksum(asset, checksum, host) {
|
|
487
|
+
const expected = parseChecksumText(await readFile(checksum, "utf8"), assetNameFor(host));
|
|
334
488
|
const actual = await sha256File(asset);
|
|
335
489
|
if (actual !== expected) {
|
|
336
490
|
fail(`release checksum mismatch: expected ${expected}, got ${actual}`);
|
|
@@ -420,8 +574,8 @@ async function verifyPackageChecksums(pluginRoot) {
|
|
|
420
574
|
return declared.size;
|
|
421
575
|
}
|
|
422
576
|
|
|
423
|
-
async function verifyExtractedPackage(pluginRoot) {
|
|
424
|
-
const manifest = await readJsonObject(join(pluginRoot,
|
|
577
|
+
async function verifyExtractedPackage(pluginRoot, host) {
|
|
578
|
+
const manifest = await readJsonObject(join(pluginRoot, HOST_LAYOUTS[host].manifestRelative));
|
|
425
579
|
if (manifest.name !== PLUGIN_NAME || manifest.version !== PRODUCT_VERSION) {
|
|
426
580
|
fail(
|
|
427
581
|
`plugin manifest mismatch: expected ${PLUGIN_NAME} ${PRODUCT_VERSION}, ` +
|
|
@@ -431,7 +585,7 @@ async function verifyExtractedPackage(pluginRoot) {
|
|
|
431
585
|
const release = await readJsonObject(join(pluginRoot, "release-manifest.json"));
|
|
432
586
|
if (
|
|
433
587
|
release.product_version !== PRODUCT_VERSION ||
|
|
434
|
-
release.host !==
|
|
588
|
+
release.host !== host ||
|
|
435
589
|
release.schema !== "opensocrates.plugin-release-manifest/1.0.0"
|
|
436
590
|
) {
|
|
437
591
|
fail("package release manifest does not match this installer");
|
|
@@ -454,10 +608,10 @@ async function prepareVerifiedPackage(options) {
|
|
|
454
608
|
const scratch = await mkdtemp(join(tmpdir(), "opensocrates-install-"));
|
|
455
609
|
try {
|
|
456
610
|
const { asset, checksum } = await resolveAssetInputs(options, scratch);
|
|
457
|
-
await verifyOuterChecksum(asset, checksum);
|
|
611
|
+
await verifyOuterChecksum(asset, checksum, options.host);
|
|
458
612
|
const pluginRoot = join(scratch, "plugin");
|
|
459
613
|
await extractArchive(asset, pluginRoot);
|
|
460
|
-
const checkedFiles = await verifyExtractedPackage(pluginRoot);
|
|
614
|
+
const checkedFiles = await verifyExtractedPackage(pluginRoot, options.host);
|
|
461
615
|
return { scratch, pluginRoot, checkedFiles };
|
|
462
616
|
} catch (error) {
|
|
463
617
|
await rm(scratch, { recursive: true, force: true });
|
|
@@ -465,7 +619,26 @@ async function prepareVerifiedPackage(options) {
|
|
|
465
619
|
}
|
|
466
620
|
}
|
|
467
621
|
|
|
468
|
-
function expectedMarketplace() {
|
|
622
|
+
function expectedMarketplace(host) {
|
|
623
|
+
if (host === "claude") {
|
|
624
|
+
return {
|
|
625
|
+
name: MARKETPLACE_NAME,
|
|
626
|
+
owner: { name: "Parker Hwang" },
|
|
627
|
+
metadata: {
|
|
628
|
+
description: "OpenSocrates reasoning support for Claude Code and Cowork",
|
|
629
|
+
version: PRODUCT_VERSION,
|
|
630
|
+
},
|
|
631
|
+
plugins: [
|
|
632
|
+
{
|
|
633
|
+
name: PLUGIN_NAME,
|
|
634
|
+
source: `./plugins/${PLUGIN_NAME}`,
|
|
635
|
+
description:
|
|
636
|
+
"Local reasoning-system selection for Claude Code and Cowork, plus reusable Claude skills.",
|
|
637
|
+
category: "workflow",
|
|
638
|
+
},
|
|
639
|
+
],
|
|
640
|
+
};
|
|
641
|
+
}
|
|
469
642
|
return {
|
|
470
643
|
name: MARKETPLACE_NAME,
|
|
471
644
|
interface: { displayName: "OpenSocrates" },
|
|
@@ -486,23 +659,24 @@ function expectedMarketplace() {
|
|
|
486
659
|
};
|
|
487
660
|
}
|
|
488
661
|
|
|
489
|
-
async function buildStagingTree(parent, pluginSource) {
|
|
662
|
+
async function buildStagingTree(parent, pluginSource, host) {
|
|
663
|
+
const layout = HOST_LAYOUTS[host];
|
|
490
664
|
const staging = await mkdtemp(join(parent, ".opensocrates.staging-"));
|
|
491
665
|
try {
|
|
492
|
-
const marketplace = join(staging,
|
|
493
|
-
const plugin = join(staging,
|
|
666
|
+
const marketplace = join(staging, layout.marketplaceRelative);
|
|
667
|
+
const plugin = join(staging, layout.pluginRelative);
|
|
494
668
|
await mkdir(dirname(marketplace), { recursive: true, mode: 0o700 });
|
|
495
669
|
await mkdir(dirname(plugin), { recursive: true, mode: 0o700 });
|
|
496
670
|
await cp(pluginSource, plugin, { recursive: true, preserveTimestamps: true });
|
|
497
|
-
await writeFile(marketplace, `${JSON.stringify(expectedMarketplace(), null, 2)}\n`, {
|
|
671
|
+
await writeFile(marketplace, `${JSON.stringify(expectedMarketplace(host), null, 2)}\n`, {
|
|
498
672
|
encoding: "utf8",
|
|
499
673
|
mode: 0o600,
|
|
500
674
|
});
|
|
501
|
-
await writeFile(join(staging, MARKER_NAME), `${JSON.stringify(
|
|
675
|
+
await writeFile(join(staging, MARKER_NAME), `${JSON.stringify(markerFor(host), null, 2)}\n`, {
|
|
502
676
|
encoding: "utf8",
|
|
503
677
|
mode: 0o600,
|
|
504
678
|
});
|
|
505
|
-
await verifyExtractedPackage(plugin);
|
|
679
|
+
await verifyExtractedPackage(plugin, host);
|
|
506
680
|
return staging;
|
|
507
681
|
} catch (error) {
|
|
508
682
|
await rm(staging, { recursive: true, force: true });
|
|
@@ -510,17 +684,38 @@ async function buildStagingTree(parent, pluginSource) {
|
|
|
510
684
|
}
|
|
511
685
|
}
|
|
512
686
|
|
|
513
|
-
function entryRoot(entry) {
|
|
687
|
+
function entryRoot(entry, host) {
|
|
514
688
|
if (entry === null) {
|
|
515
689
|
return null;
|
|
516
690
|
}
|
|
517
|
-
|
|
518
|
-
|
|
691
|
+
const value = host === "claude" ? entry.path ?? entry.installLocation : entry.root;
|
|
692
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
693
|
+
fail(`${host} marketplace ${MARKETPLACE_NAME} has no usable root`);
|
|
694
|
+
}
|
|
695
|
+
const resolved = resolve(value);
|
|
696
|
+
try {
|
|
697
|
+
// Host CLIs may report an equivalent canonical path even when the user
|
|
698
|
+
// configured a symlinked home (for example /var versus /private/var on
|
|
699
|
+
// macOS). Compare canonical existing paths without weakening ownership
|
|
700
|
+
// checks for missing or unmanaged roots.
|
|
701
|
+
return realpathSync(resolved);
|
|
702
|
+
} catch {
|
|
703
|
+
return resolved;
|
|
519
704
|
}
|
|
520
|
-
return resolve(entry.root);
|
|
521
705
|
}
|
|
522
706
|
|
|
523
|
-
function removeRegistration(entry, state) {
|
|
707
|
+
function removeRegistration(host, entry, state) {
|
|
708
|
+
if (host === "claude") {
|
|
709
|
+
if (state.kind === "installed") {
|
|
710
|
+
run(claudeBinary(), ["plugin", "uninstall", PLUGIN_ID, "--scope", "user"]);
|
|
711
|
+
}
|
|
712
|
+
if (entry !== null) {
|
|
713
|
+
run(claudeBinary(), [
|
|
714
|
+
"plugin", "marketplace", "remove", MARKETPLACE_NAME, "--scope", "user",
|
|
715
|
+
]);
|
|
716
|
+
}
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
524
719
|
if (state.kind === "installed") {
|
|
525
720
|
runCodexJson(["plugin", "remove", PLUGIN_ID, "--json"]);
|
|
526
721
|
}
|
|
@@ -529,7 +724,14 @@ function removeRegistration(entry, state) {
|
|
|
529
724
|
}
|
|
530
725
|
}
|
|
531
726
|
|
|
532
|
-
function addRegistration(root, installPlugin) {
|
|
727
|
+
function addRegistration(host, root, installPlugin) {
|
|
728
|
+
if (host === "claude") {
|
|
729
|
+
run(claudeBinary(), ["plugin", "marketplace", "add", root, "--scope", "user"]);
|
|
730
|
+
if (installPlugin) {
|
|
731
|
+
run(claudeBinary(), ["plugin", "install", PLUGIN_ID, "--scope", "user"]);
|
|
732
|
+
}
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
533
735
|
runCodexJson(["plugin", "marketplace", "add", root, "--json"]);
|
|
534
736
|
if (installPlugin) {
|
|
535
737
|
return runCodexJson(["plugin", "add", PLUGIN_ID, "--json"]);
|
|
@@ -537,10 +739,10 @@ function addRegistration(root, installPlugin) {
|
|
|
537
739
|
return null;
|
|
538
740
|
}
|
|
539
741
|
|
|
540
|
-
function removeRegistrationBestEffort() {
|
|
742
|
+
function removeRegistrationBestEffort(host) {
|
|
541
743
|
const entry = (() => {
|
|
542
744
|
try {
|
|
543
|
-
return marketplaceEntry();
|
|
745
|
+
return marketplaceEntry(host);
|
|
544
746
|
} catch {
|
|
545
747
|
return null;
|
|
546
748
|
}
|
|
@@ -549,36 +751,60 @@ function removeRegistrationBestEffort() {
|
|
|
549
751
|
return;
|
|
550
752
|
}
|
|
551
753
|
try {
|
|
552
|
-
const state = pluginState();
|
|
754
|
+
const state = pluginState(host);
|
|
553
755
|
if (state.kind === "installed") {
|
|
554
|
-
|
|
756
|
+
const binary = host === "claude" ? claudeBinary() : codexBinary();
|
|
757
|
+
const args =
|
|
758
|
+
host === "claude"
|
|
759
|
+
? ["plugin", "uninstall", PLUGIN_ID, "--scope", "user"]
|
|
760
|
+
: ["plugin", "remove", PLUGIN_ID, "--json"];
|
|
761
|
+
run(binary, args, { allowFailure: true });
|
|
555
762
|
}
|
|
556
763
|
} catch {
|
|
557
764
|
// Rollback continues with marketplace cleanup.
|
|
558
765
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
766
|
+
const binary = host === "claude" ? claudeBinary() : codexBinary();
|
|
767
|
+
const args =
|
|
768
|
+
host === "claude"
|
|
769
|
+
? ["plugin", "marketplace", "remove", MARKETPLACE_NAME, "--scope", "user"]
|
|
770
|
+
: ["plugin", "marketplace", "remove", MARKETPLACE_NAME, "--json"];
|
|
771
|
+
run(binary, args, { allowFailure: true });
|
|
562
772
|
}
|
|
563
773
|
|
|
564
|
-
async function
|
|
565
|
-
|
|
566
|
-
|
|
774
|
+
async function recoveryStep(label, action) {
|
|
775
|
+
try {
|
|
776
|
+
await action();
|
|
777
|
+
return true;
|
|
778
|
+
} catch (recoveryError) {
|
|
779
|
+
const detail =
|
|
780
|
+
recoveryError instanceof Error ? recoveryError.message : String(recoveryError);
|
|
781
|
+
console.error(`warning: rollback step failed (${label}): ${detail}`);
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async function installVerifiedPackage(pluginSource, action, host) {
|
|
787
|
+
requireHost(host);
|
|
788
|
+
if (host === "claude") {
|
|
789
|
+
requireNoLegacyClaudeInstallation();
|
|
790
|
+
}
|
|
791
|
+
const paths = managedPaths(host);
|
|
567
792
|
await mkdir(paths.parent, { recursive: true, mode: 0o700 });
|
|
568
793
|
|
|
569
|
-
const previousEntry = marketplaceEntry();
|
|
570
|
-
if (previousEntry !== null && entryRoot(previousEntry) !== paths.root) {
|
|
794
|
+
const previousEntry = marketplaceEntry(host);
|
|
795
|
+
if (previousEntry !== null && entryRoot(previousEntry, host) !== paths.root) {
|
|
571
796
|
fail(
|
|
572
|
-
`marketplace ${MARKETPLACE_NAME} is already registered at ${entryRoot(previousEntry)}; ` +
|
|
797
|
+
`marketplace ${MARKETPLACE_NAME} is already registered at ${entryRoot(previousEntry, host)}; ` +
|
|
573
798
|
"refusing to overwrite an unmanaged location",
|
|
574
799
|
);
|
|
575
800
|
}
|
|
576
|
-
const previousState =
|
|
801
|
+
const previousState =
|
|
802
|
+
previousEntry === null ? { kind: "missing", version: null } : pluginState(host);
|
|
577
803
|
if (await exists(paths.root)) {
|
|
578
|
-
await requireOwnedRoot(paths.root);
|
|
804
|
+
await requireOwnedRoot(paths.root, host);
|
|
579
805
|
}
|
|
580
806
|
|
|
581
|
-
const staging = await buildStagingTree(paths.parent, pluginSource);
|
|
807
|
+
const staging = await buildStagingTree(paths.parent, pluginSource, host);
|
|
582
808
|
const backup = join(paths.parent, `.opensocrates.backup-${randomUUID()}`);
|
|
583
809
|
let registrationRemoved = false;
|
|
584
810
|
let backupCreated = false;
|
|
@@ -586,7 +812,7 @@ async function installVerifiedPackage(pluginSource, action) {
|
|
|
586
812
|
try {
|
|
587
813
|
if (previousEntry !== null) {
|
|
588
814
|
registrationRemoved = true;
|
|
589
|
-
removeRegistration(previousEntry, previousState);
|
|
815
|
+
removeRegistration(host, previousEntry, previousState);
|
|
590
816
|
}
|
|
591
817
|
if (await exists(paths.root)) {
|
|
592
818
|
await rename(paths.root, backup);
|
|
@@ -594,40 +820,70 @@ async function installVerifiedPackage(pluginSource, action) {
|
|
|
594
820
|
}
|
|
595
821
|
await rename(staging, paths.root);
|
|
596
822
|
newRootActive = true;
|
|
597
|
-
const result = addRegistration(paths.root, true);
|
|
598
|
-
const state = pluginState();
|
|
823
|
+
const result = addRegistration(host, paths.root, true);
|
|
824
|
+
const state = pluginState(host);
|
|
599
825
|
if (
|
|
600
|
-
|
|
601
|
-
|
|
826
|
+
(host === "codex" &&
|
|
827
|
+
(result?.pluginId !== PLUGIN_ID || result?.version !== PRODUCT_VERSION)) ||
|
|
602
828
|
state.kind !== "installed" ||
|
|
603
829
|
state.version !== PRODUCT_VERSION
|
|
604
830
|
) {
|
|
605
|
-
fail(
|
|
831
|
+
fail(`${host} did not confirm the expected installed plugin and version`);
|
|
606
832
|
}
|
|
607
833
|
if (backupCreated) {
|
|
608
|
-
await requireOwnedRoot(backup);
|
|
834
|
+
await requireOwnedRoot(backup, host);
|
|
609
835
|
await rm(backup, { recursive: true });
|
|
610
836
|
}
|
|
611
837
|
const verb = action === "update" ? "updated" : "installed";
|
|
612
838
|
console.log(`OpenSocrates ${PRODUCT_VERSION} ${verb} successfully.`);
|
|
613
839
|
console.log(`Managed marketplace: ${paths.root}`);
|
|
614
|
-
console.log(
|
|
840
|
+
console.log(
|
|
841
|
+
host === "claude"
|
|
842
|
+
? "Start a new Claude Code or Cowork task to load the updated skills and hooks."
|
|
843
|
+
: "Start a new Codex task to load the updated skills and hooks.",
|
|
844
|
+
);
|
|
615
845
|
} catch (error) {
|
|
616
|
-
|
|
846
|
+
// Every recovery stage is independently guarded: one failing stage must
|
|
847
|
+
// never prevent the later stages from running, because the stage that
|
|
848
|
+
// restores the user's previous installation comes last.
|
|
849
|
+
let restored = false;
|
|
850
|
+
await recoveryStep("unregister the failed installation", async () => {
|
|
851
|
+
removeRegistrationBestEffort(host);
|
|
852
|
+
});
|
|
617
853
|
if (newRootActive && (await exists(paths.root))) {
|
|
618
|
-
await
|
|
619
|
-
|
|
854
|
+
await recoveryStep("remove the failed installation root", async () => {
|
|
855
|
+
// This run created paths.root by renaming staging into place, so its
|
|
856
|
+
// provenance is known directly and the ownership marker is redundant
|
|
857
|
+
// here. Requiring the marker would let a host that corrupted or
|
|
858
|
+
// removed it while failing block recovery entirely, because the
|
|
859
|
+
// surviving directory then also blocks the backup rename below.
|
|
860
|
+
// Roots this run did not create remain guarded by requireOwnedRoot
|
|
861
|
+
// before staging begins, and the backup restore below still verifies
|
|
862
|
+
// ownership.
|
|
863
|
+
await rm(paths.root, { recursive: true, force: true });
|
|
864
|
+
});
|
|
620
865
|
}
|
|
621
866
|
if (backupCreated && (await exists(backup))) {
|
|
622
|
-
await
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
}
|
|
867
|
+
restored = await recoveryStep("restore the previous installation", async () => {
|
|
868
|
+
await requireOwnedRoot(backup, host);
|
|
869
|
+
await rename(backup, paths.root);
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
if (registrationRemoved && previousEntry !== null && (restored || !backupCreated)) {
|
|
873
|
+
await recoveryStep("re-register the previous installation", async () => {
|
|
874
|
+
addRegistration(host, paths.root, previousState.kind === "installed");
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
if (backupCreated && !restored) {
|
|
878
|
+
console.error(
|
|
879
|
+
"error: the previous OpenSocrates installation could not be restored automatically.",
|
|
880
|
+
);
|
|
881
|
+
console.error(`error: your previous files are preserved at: ${backup}`);
|
|
882
|
+
console.error(
|
|
883
|
+
`error: remove ${paths.root} if it still exists, move the preserved directory ` +
|
|
884
|
+
`to ${paths.root}, then rerun ` +
|
|
885
|
+
`\`opensocrates install --host ${host}\`.`,
|
|
886
|
+
);
|
|
631
887
|
}
|
|
632
888
|
throw error;
|
|
633
889
|
} finally {
|
|
@@ -637,24 +893,27 @@ async function installVerifiedPackage(pluginSource, action) {
|
|
|
637
893
|
}
|
|
638
894
|
}
|
|
639
895
|
|
|
640
|
-
async function showStatus() {
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
896
|
+
async function showStatus(host) {
|
|
897
|
+
requireHost(host);
|
|
898
|
+
if (host === "claude") {
|
|
899
|
+
warnLegacyClaudeInstallation();
|
|
900
|
+
}
|
|
901
|
+
const paths = managedPaths(host);
|
|
902
|
+
const entry = marketplaceEntry(host);
|
|
644
903
|
if (entry === null) {
|
|
645
904
|
if (await exists(paths.root)) {
|
|
646
|
-
await requireOwnedRoot(paths.root);
|
|
905
|
+
await requireOwnedRoot(paths.root, host);
|
|
647
906
|
console.log(`OpenSocrates files are present but not registered: ${paths.root}`);
|
|
648
907
|
} else {
|
|
649
908
|
console.log("OpenSocrates is not installed.");
|
|
650
909
|
}
|
|
651
910
|
return;
|
|
652
911
|
}
|
|
653
|
-
if (entryRoot(entry) !== paths.root) {
|
|
654
|
-
fail(`OpenSocrates is registered at an unmanaged location: ${entryRoot(entry)}`);
|
|
912
|
+
if (entryRoot(entry, host) !== paths.root) {
|
|
913
|
+
fail(`OpenSocrates is registered at an unmanaged location: ${entryRoot(entry, host)}`);
|
|
655
914
|
}
|
|
656
|
-
await requireOwnedRoot(paths.root);
|
|
657
|
-
const state = pluginState();
|
|
915
|
+
await requireOwnedRoot(paths.root, host);
|
|
916
|
+
const state = pluginState(host);
|
|
658
917
|
if (state.kind === "installed") {
|
|
659
918
|
console.log(`OpenSocrates ${state.version ?? "unknown"} is installed.`);
|
|
660
919
|
} else if (state.kind === "available") {
|
|
@@ -664,20 +923,25 @@ async function showStatus() {
|
|
|
664
923
|
}
|
|
665
924
|
}
|
|
666
925
|
|
|
667
|
-
async function removeInstalled() {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
926
|
+
async function removeInstalled(host) {
|
|
927
|
+
requireHost(host);
|
|
928
|
+
if (host === "claude") {
|
|
929
|
+
// Warn only. A legacy registration must never block removal of the root
|
|
930
|
+
// this installer owns, and removal never touches the legacy installation.
|
|
931
|
+
warnLegacyClaudeInstallation();
|
|
932
|
+
}
|
|
933
|
+
const paths = managedPaths(host);
|
|
934
|
+
const entry = marketplaceEntry(host);
|
|
935
|
+
if (entry !== null && entryRoot(entry, host) !== paths.root) {
|
|
936
|
+
fail(`OpenSocrates is registered at an unmanaged location: ${entryRoot(entry, host)}`);
|
|
673
937
|
}
|
|
674
|
-
const state = entry === null ? { kind: "missing", version: null } : pluginState();
|
|
675
|
-
removeRegistration(entry, state);
|
|
938
|
+
const state = entry === null ? { kind: "missing", version: null } : pluginState(host);
|
|
939
|
+
removeRegistration(host, entry, state);
|
|
676
940
|
if (await exists(paths.root)) {
|
|
677
|
-
await requireOwnedRoot(paths.root);
|
|
941
|
+
await requireOwnedRoot(paths.root, host);
|
|
678
942
|
await rm(paths.root, { recursive: true });
|
|
679
943
|
}
|
|
680
|
-
console.log(
|
|
944
|
+
console.log(`OpenSocrates was removed from ${host === "claude" ? "Claude" : "Codex"}.`);
|
|
681
945
|
}
|
|
682
946
|
|
|
683
947
|
export async function main(argv = process.argv.slice(2)) {
|
|
@@ -687,11 +951,11 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
687
951
|
return 0;
|
|
688
952
|
}
|
|
689
953
|
if (options.action === "status") {
|
|
690
|
-
await showStatus();
|
|
954
|
+
await showStatus(options.host);
|
|
691
955
|
return 0;
|
|
692
956
|
}
|
|
693
957
|
if (options.action === "remove") {
|
|
694
|
-
await removeInstalled();
|
|
958
|
+
await removeInstalled(options.host);
|
|
695
959
|
return 0;
|
|
696
960
|
}
|
|
697
961
|
if (
|
|
@@ -712,7 +976,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
712
976
|
if (options.action === "verify") {
|
|
713
977
|
return 0;
|
|
714
978
|
}
|
|
715
|
-
await installVerifiedPackage(prepared.pluginRoot, options.action);
|
|
979
|
+
await installVerifiedPackage(prepared.pluginRoot, options.action, options.host);
|
|
716
980
|
return 0;
|
|
717
981
|
} finally {
|
|
718
982
|
await rm(prepared.scratch, { recursive: true, force: true });
|