subconscious-cli 6.2.0 → 6.3.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/README.md +10 -18
- package/bin/agent-launch.js +37 -4
- package/bin/agents.js +18 -0
- package/bin/auth.js +1 -1
- package/bin/claude-config-drift.js +58 -0
- package/bin/claude-config-plugins.js +103 -0
- package/bin/claude-config.js +624 -0
- package/bin/cli.js +35 -4
- package/bin/command-output.js +75 -0
- package/bin/native/subc-tui-darwin-amd64 +0 -0
- package/bin/native/subc-tui-darwin-arm64 +0 -0
- package/bin/native/subc-tui-linux-amd64 +0 -0
- package/bin/native/subc-tui-linux-arm64 +0 -0
- package/bin/native/subc-tui-windows-amd64.exe +0 -0
- package/bin/native/subc-tui-windows-arm64.exe +0 -0
- package/bin/profiles.js +4 -0
- package/bin/runbook/README.md +2 -3
- package/bin/runbook/claude-code/agent.json +25 -1
- package/bin/runbook/shared.json +1 -2
- package/bin/sessions.js +24 -27
- package/bin/tui.js +18 -8
- package/bin/windows/hook.cjs +4 -10
- package/bin/windows/launch.js +22 -9
- package/bin/windows/process.js +1 -18
- package/bin/windows/setup.js +2 -8
- package/package.json +3 -4
- package/bin/runbook/cursor/agent.json +0 -138
- package/bin/runbook/cursor/hook.sh +0 -145
- package/bin/runbook/cursor/hooks.json +0 -17
- package/bin/runbook/cursor/install.sh +0 -259
package/README.md
CHANGED
|
@@ -60,7 +60,6 @@ so OpenCode, Claude Code, Codex, and DeepSeek Harness can launch immediately.
|
|
|
60
60
|
Persistent editor and Pi integrations are installed per agent:
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
|
-
subc cursor install
|
|
64
63
|
subc copilot install
|
|
65
64
|
subc pi
|
|
66
65
|
subc <agent> uninstall
|
|
@@ -90,7 +89,7 @@ profile; they do not authenticate, install, configure, or launch anything:
|
|
|
90
89
|
subc opencode help
|
|
91
90
|
subc claude help
|
|
92
91
|
subc codex help
|
|
93
|
-
subc
|
|
92
|
+
subc copilot help
|
|
94
93
|
subc config help
|
|
95
94
|
subc login help
|
|
96
95
|
```
|
|
@@ -127,7 +126,6 @@ subc.cmd claude
|
|
|
127
126
|
subc codex
|
|
128
127
|
subc opencode
|
|
129
128
|
subc dsh
|
|
130
|
-
subc cursor install
|
|
131
129
|
subc copilot install
|
|
132
130
|
subc pi
|
|
133
131
|
subc config edit notepad
|
|
@@ -155,7 +153,7 @@ and the native TUI use Windows paths and processes. Profile editing defaults to
|
|
|
155
153
|
Notepad, honors `VISUAL`/`EDITOR`, and supports `code`/`code-insiders` with `--wait`.
|
|
156
154
|
The existing profile location remains `%USERPROFILE%\.subconscious`.
|
|
157
155
|
|
|
158
|
-
|
|
156
|
+
Copilot, Codex, and Pi setup use native JSON merges and Node hooks. Other
|
|
159
157
|
providers and hooks are preserved; malformed configuration is left unchanged.
|
|
160
158
|
Hook credentials are stored in `subconscious-windows.json` within the agent's
|
|
161
159
|
user directory; treat that file as a secret. Copilot's model provider still uses
|
|
@@ -176,9 +174,8 @@ The packaged integrations live in `bin/runbook`.
|
|
|
176
174
|
| `subc opencode` | Launch OpenCode with the runbook provider, client header, and context/output limits |
|
|
177
175
|
| `subc claude` | Launch Claude Code with the runbook environment, context limits, subagent limits, and OTEL usage reporting |
|
|
178
176
|
| `subc codex` | Launch Codex with the runbook provider, temporary model catalog, and surgically merged compaction hooks |
|
|
179
|
-
| `subc cursor install` | Install/update Cursor conversation and compaction hooks (Cursor desktop app and IDE only — the Cursor CLI is not supported) |
|
|
180
|
-
| `subc copilot install` | Install/update the VS Code custom endpoint and Copilot hooks |
|
|
181
177
|
| `subc pi` | Refresh the Pi provider from the live catalog, then launch |
|
|
178
|
+
| `subc copilot install` | Install/update the VS Code custom endpoint and Copilot hooks |
|
|
182
179
|
| `subc dsh` | Launch the DeepSeek Harness Web UI with a temporary provider populated from the live catalog |
|
|
183
180
|
|
|
184
181
|
## Headless runs
|
|
@@ -323,8 +320,8 @@ characters. Tool payloads, tool results, system prompts, and hidden reasoning
|
|
|
323
320
|
are not copied. Source transcripts remain owned by their original harness and
|
|
324
321
|
are never rewritten.
|
|
325
322
|
|
|
326
|
-
|
|
327
|
-
conversation
|
|
323
|
+
GitHub Copilot sessions are not listed because its IDE-owned
|
|
324
|
+
conversation store does not expose a stable local resume interface. A session
|
|
328
325
|
whose local transcript is missing remains available for native resume but does
|
|
329
326
|
not offer cross-harness destinations.
|
|
330
327
|
|
|
@@ -346,7 +343,6 @@ Persistent writes are merge/unmerge only. They never replace a user's
|
|
|
346
343
|
provider list. Remove Subconscious files with the matching uninstall command:
|
|
347
344
|
|
|
348
345
|
```bash
|
|
349
|
-
subc cursor uninstall
|
|
350
346
|
subc copilot uninstall
|
|
351
347
|
subc pi uninstall
|
|
352
348
|
subc codex uninstall
|
|
@@ -362,14 +358,13 @@ Inspect a persistent integration with `subc <agent> status`. A one-off
|
|
|
362
358
|
`--api-key` passed to install takes precedence for that command but is not
|
|
363
359
|
saved to the selected profile.
|
|
364
360
|
|
|
365
|
-
Cursor still requires enabling its OpenAI API Key Override in Cursor Settings.
|
|
366
361
|
Copilot requires entering the custom endpoint key once through VS Code's
|
|
367
362
|
Manage Language Models UI. The install scripts print the relevant next steps.
|
|
368
363
|
Its Custom Endpoint provider uses the Messages API at `/v1/messages` by
|
|
369
364
|
default; rerun `subc copilot install` to migrate an older Chat Completions entry.
|
|
370
365
|
|
|
371
|
-
The runbook scripts require Bash.
|
|
372
|
-
also require `jq
|
|
366
|
+
The runbook scripts require Bash. Copilot, Pi, and Codex hook merge
|
|
367
|
+
also require `jq`. Copilot and Codex hooks also require `curl`.
|
|
373
368
|
|
|
374
369
|
`subc dsh` defaults to the DeepSeek Harness Web UI. It injects an ephemeral
|
|
375
370
|
Cordis overlay containing the active gateway URL, selected model, token limits,
|
|
@@ -434,7 +429,7 @@ subc -p staging config \
|
|
|
434
429
|
--model subconscious/glm-5.3-marathon
|
|
435
430
|
|
|
436
431
|
subc -p staging claude
|
|
437
|
-
subc -p staging
|
|
432
|
+
subc -p staging copilot install
|
|
438
433
|
```
|
|
439
434
|
|
|
440
435
|
You can also select one with `SUBC_PROFILE=staging`. Explicit shell variables
|
|
@@ -468,7 +463,7 @@ export SUBCONSCIOUS_MODEL=subconscious/glm-5.3-marathon
|
|
|
468
463
|
```
|
|
469
464
|
|
|
470
465
|
Every launch and install fetches the same live catalog without caching. Codex,
|
|
471
|
-
OpenCode, Pi, Copilot,
|
|
466
|
+
OpenCode, Pi, Copilot, and DeepSeek Harness receive the complete model
|
|
472
467
|
list. The selected profile model is listed first only while the gateway still
|
|
473
468
|
advertises it.
|
|
474
469
|
OpenCode rebuilds its isolated runtime provider on every `subc opencode` launch,
|
|
@@ -481,10 +476,7 @@ custom model option. The CLI remaps those aliases to the live catalog, then
|
|
|
481
476
|
replaces the built-in `/model` lineup (`modelPicker.replaceBuiltInOptions`) and
|
|
482
477
|
allowlists only those models (`availableModels`) so Anthropic Fable and
|
|
483
478
|
ungranted slugs do not appear. Any other model can still be selected with
|
|
484
|
-
`subc claude --model MODEL`.
|
|
485
|
-
the printed model IDs in its OpenAI API Key Override settings. Use the printed
|
|
486
|
-
`/v1` Base URL in Cursor Settings; the profile itself stores the gateway origin
|
|
487
|
-
so correlation hooks can post to `/v1/agent-hooks`.
|
|
479
|
+
`subc claude --model MODEL`.
|
|
488
480
|
|
|
489
481
|
The default gateway is `https://api.subconscious.dev`. Profiles containing
|
|
490
482
|
the former exact default (`https://api.subconscious.dev`) migrate automatically;
|
package/bin/agent-launch.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
1
2
|
import { agentInputs, agentKeyName } from './agent-data.js';
|
|
2
3
|
import { openCodeConfigFromEnv } from './opencode-provider.js';
|
|
3
|
-
import { isUnsetSetting } from './profiles.js';
|
|
4
|
+
import { configDir, isUnsetSetting } from './profiles.js';
|
|
4
5
|
|
|
5
6
|
// One pass, so a substituted value (a prompt, a settings document) is never
|
|
6
7
|
// scanned again for placeholders.
|
|
@@ -8,6 +9,9 @@ const PLACEHOLDER = /\$\{([A-Z_][A-Z0-9_]*)\}(\/)?|\{([A-Za-z][A-Za-z0-9]*)\}/g;
|
|
|
8
9
|
const present = (value) =>
|
|
9
10
|
value !== undefined && value !== null && value !== '';
|
|
10
11
|
|
|
12
|
+
// CLAUDE_CODE_ISOLATE values that stop Claude Code config isolation.
|
|
13
|
+
const ISOLATE_OFF = new Set(['off', '0', 'false', 'no']);
|
|
14
|
+
|
|
11
15
|
function tokensFor(agent, env, extra) {
|
|
12
16
|
const baseUrl = String(env.GATEWAY_URL ?? '').replace(/\/+$/, '');
|
|
13
17
|
const keyName = agentKeyName(agent);
|
|
@@ -19,6 +23,26 @@ function tokensFor(agent, env, extra) {
|
|
|
19
23
|
// Left for the runbook, which writes the file and substitutes its path.
|
|
20
24
|
tempFile: '{tempFile}',
|
|
21
25
|
opencodeConfig: () => JSON.stringify(openCodeConfigFromEnv(env)),
|
|
26
|
+
// Claude Code config isolation: subc sessions keep their Claude config
|
|
27
|
+
// (settings, sessions, plugins, credentials) under the subc config dir
|
|
28
|
+
// instead of ~/.claude, so session state cannot bleed into a plain
|
|
29
|
+
// `claude` installation. Empty when CLAUDE_CODE_ISOLATE opts out.
|
|
30
|
+
claudeConfigDir: () => {
|
|
31
|
+
const setting = String(env.CLAUDE_CODE_ISOLATE ?? '')
|
|
32
|
+
.trim()
|
|
33
|
+
.toLowerCase();
|
|
34
|
+
// Opted out: the user's own folder, as plain `claude` would use it.
|
|
35
|
+
if (ISOLATE_OFF.has(setting)) {
|
|
36
|
+
return String(
|
|
37
|
+
env.SUBC_CLAUDE_CONFIG_SOURCE || env.CLAUDE_CONFIG_DIR || '',
|
|
38
|
+
).trim();
|
|
39
|
+
}
|
|
40
|
+
const base = String(env.SUBC_CONFIG_DIR ?? '').trim() || configDir();
|
|
41
|
+
// A relative base would put the config, MCP tokens included, in the
|
|
42
|
+
// current directory; use ~/.claude unchanged instead.
|
|
43
|
+
if (!path.isAbsolute(base)) return '';
|
|
44
|
+
return path.join(base, 'claude-code');
|
|
45
|
+
},
|
|
22
46
|
...extra,
|
|
23
47
|
};
|
|
24
48
|
}
|
|
@@ -116,15 +140,24 @@ export function resolveInputs(
|
|
|
116
140
|
return resolved;
|
|
117
141
|
}
|
|
118
142
|
|
|
119
|
-
/**
|
|
143
|
+
/**
|
|
144
|
+
* Set each env entry: the first non-empty override, otherwise its value.
|
|
145
|
+
* An entry marked `optional` is unset when its value renders empty, so a
|
|
146
|
+
* template can drop a variable instead of exporting an empty string.
|
|
147
|
+
*/
|
|
120
148
|
export function resolveAgentEnv(agent, env) {
|
|
121
149
|
const resolved = { ...env };
|
|
122
150
|
for (const entry of agent.env) {
|
|
123
151
|
const override = (entry.override || [])
|
|
124
152
|
.map((name) => resolved[name])
|
|
125
153
|
.find(present);
|
|
126
|
-
|
|
127
|
-
|
|
154
|
+
if (override !== undefined) {
|
|
155
|
+
resolved[entry.name] = override;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const value = render(entry.value, agent, resolved);
|
|
159
|
+
if (entry.optional && !present(value)) delete resolved[entry.name];
|
|
160
|
+
else resolved[entry.name] = value;
|
|
128
161
|
}
|
|
129
162
|
return resolved;
|
|
130
163
|
}
|
package/bin/agents.js
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import { launchPlan, resolveInputs } from './agent-launch.js';
|
|
20
20
|
import { planCommand, runCommand, scriptCommand } from './agent-spawn.js';
|
|
21
21
|
import { getApiKey } from './auth.js';
|
|
22
|
+
import { prepareClaudeLaunch } from './claude-config.js';
|
|
22
23
|
import {
|
|
23
24
|
applyClaudePickerCatalog,
|
|
24
25
|
CLAUDE_PICKER_ENV_DESCRIPTION,
|
|
@@ -545,5 +546,22 @@ async function launchAgent(agent, argv, profile, headless) {
|
|
|
545
546
|
// The headless endpoint is the one endpoint for every harness, Claude too.
|
|
546
547
|
const launchEnv = headless ? { ...env, CLAUDE_GATEWAY_URL: '' } : env;
|
|
547
548
|
const plan = launchPlan(agent, { env: launchEnv, ...headlessParts(rest) });
|
|
549
|
+
if (agent.id === CLAUDE_CODE) {
|
|
550
|
+
const {
|
|
551
|
+
warnings,
|
|
552
|
+
refused,
|
|
553
|
+
env: sourceEnv,
|
|
554
|
+
} = prepareClaudeLaunch(agent, plan.env, launchEnv);
|
|
555
|
+
const color = refused ? c.red : c.yellow;
|
|
556
|
+
for (const warning of warnings) {
|
|
557
|
+
console.error(` ${color}${warning}${c.reset}`);
|
|
558
|
+
}
|
|
559
|
+
if (refused) {
|
|
560
|
+
process.exitCode = 1;
|
|
561
|
+
return 1;
|
|
562
|
+
}
|
|
563
|
+
const prepared = { ...plan, env: { ...plan.env, ...sourceEnv } };
|
|
564
|
+
return runCommand(planCommand(agent, prepared, binDir));
|
|
565
|
+
}
|
|
548
566
|
return runCommand(planCommand(agent, plan, binDir));
|
|
549
567
|
}
|
package/bin/auth.js
CHANGED
|
@@ -277,7 +277,7 @@ export async function loginCommand(_argv = [], options = {}) {
|
|
|
277
277
|
}
|
|
278
278
|
console.log(` ${c.dim}Runbook profile: ${profile.path}${c.reset}`);
|
|
279
279
|
console.log(
|
|
280
|
-
` ${c.dim}Launch a terminal agent with ${c.reset}${c.cyan}subc claude${c.reset}${c.dim}, or
|
|
280
|
+
` ${c.dim}Launch a terminal agent with ${c.reset}${c.cyan}subc claude${c.reset}${c.dim}, or set up VS Code Copilot with ${c.reset}${c.cyan}subc copilot install${c.reset}${c.dim}.${c.reset}`,
|
|
281
281
|
);
|
|
282
282
|
console.log(
|
|
283
283
|
` ${c.dim}Pi needs ${c.reset}${c.cyan}subc pi install${c.reset}${c.dim} first. List profiles with ${c.reset}${c.cyan}subc config${c.reset}${c.dim}.${c.reset}`,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
+
|
|
3
|
+
// Each launch rebuilds the subc copies of settings.json and the MCP servers
|
|
4
|
+
// in .claude.json from the user's own files, so anything a subc session
|
|
5
|
+
// changed in those copies is replaced. These helpers find what a session
|
|
6
|
+
// changed, by comparing the copies with what the last launch wrote, so the
|
|
7
|
+
// launch can save it aside and name it instead of losing it silently.
|
|
8
|
+
|
|
9
|
+
const isObject = (value) =>
|
|
10
|
+
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Keys of `current` that a session added or changed since `written`.
|
|
14
|
+
* Removed keys are ignored: Claude Code drops keys on its own.
|
|
15
|
+
*
|
|
16
|
+
* @param {Record<string, unknown>} written what the last launch wrote
|
|
17
|
+
* @param {Record<string, unknown>} current what the copy holds now
|
|
18
|
+
* @param {string[]} [ignored] keys subc keeps across launches
|
|
19
|
+
* @returns {string[]}
|
|
20
|
+
*/
|
|
21
|
+
export function changedKeys(written, current, ignored = []) {
|
|
22
|
+
if (!isObject(written) || !isObject(current)) return [];
|
|
23
|
+
return Object.keys(current).filter(
|
|
24
|
+
(key) =>
|
|
25
|
+
!ignored.includes(key) && !isDeepStrictEqual(current[key], written[key]),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The MCP servers of a .claude.json: user scope and each project. */
|
|
30
|
+
export function mcpServersOf(state) {
|
|
31
|
+
const projects = isObject(state?.projects) ? state.projects : {};
|
|
32
|
+
return {
|
|
33
|
+
mcpServers: isObject(state?.mcpServers) ? state.mcpServers : {},
|
|
34
|
+
projects: Object.fromEntries(
|
|
35
|
+
Object.entries(projects)
|
|
36
|
+
.filter(([, project]) => isObject(project?.mcpServers))
|
|
37
|
+
.map(([name, project]) => [name, project.mcpServers]),
|
|
38
|
+
),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* MCP server names a session added or changed since `written`, as
|
|
44
|
+
* `name` for user scope and `name (project)` for a project.
|
|
45
|
+
*
|
|
46
|
+
* @param {ReturnType<typeof mcpServersOf>} written
|
|
47
|
+
* @param {ReturnType<typeof mcpServersOf>} current
|
|
48
|
+
* @returns {string[]}
|
|
49
|
+
*/
|
|
50
|
+
export function changedMcpServers(written, current) {
|
|
51
|
+
const changed = changedKeys(written.mcpServers, current.mcpServers);
|
|
52
|
+
for (const [project, servers] of Object.entries(current.projects)) {
|
|
53
|
+
for (const name of changedKeys(written.projects[project] ?? {}, servers)) {
|
|
54
|
+
changed.push(`${name} (${project})`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return changed;
|
|
58
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
// Claude Code finds an enabled plugin that installed_plugins.json does not
|
|
6
|
+
// list (a marketplace plugin pinned to a git SHA, for example) by its place
|
|
7
|
+
// in its own cache: <config dir>/plugins/cache/<marketplace>/<plugin>/<version>.
|
|
8
|
+
// The subc config dir never links that cache, so such a plugin, and the MCP
|
|
9
|
+
// servers it ships, would not load. Each missing version is copied once. A
|
|
10
|
+
// copy, unlike a link, cannot carry a cache sweep or orphan marker made in a
|
|
11
|
+
// subc session back into ~/.claude.
|
|
12
|
+
|
|
13
|
+
// A marketplace or plugin name used as one path segment.
|
|
14
|
+
const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
15
|
+
|
|
16
|
+
/** `[plugin, marketplace]` of an enabledPlugins key, or `undefined`. */
|
|
17
|
+
function parsePluginId(id) {
|
|
18
|
+
const at = id.lastIndexOf('@');
|
|
19
|
+
if (at <= 0) return undefined;
|
|
20
|
+
const plugin = id.slice(0, at);
|
|
21
|
+
const marketplace = id.slice(at + 1);
|
|
22
|
+
return SAFE_NAME.test(plugin) && SAFE_NAME.test(marketplace)
|
|
23
|
+
? [plugin, marketplace]
|
|
24
|
+
: undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function copyVersion(from, to) {
|
|
28
|
+
// A copy is renamed into place only when complete, so a launch stopped
|
|
29
|
+
// half way never leaves a partial plugin that later launches skip.
|
|
30
|
+
const temp = `${to}.${crypto.randomBytes(6).toString('hex')}.tmp`;
|
|
31
|
+
try {
|
|
32
|
+
fs.cpSync(from, temp, { recursive: true, verbatimSymlinks: true });
|
|
33
|
+
fs.renameSync(temp, to);
|
|
34
|
+
} finally {
|
|
35
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Whether `base` or any folder from it down through `parts` is a link. */
|
|
40
|
+
function linkOnPath(base, parts) {
|
|
41
|
+
let current = base;
|
|
42
|
+
for (const part of ['', ...parts]) {
|
|
43
|
+
current = part ? path.join(current, part) : current;
|
|
44
|
+
try {
|
|
45
|
+
if (fs.lstatSync(current).isSymbolicLink()) return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function directories(dir) {
|
|
54
|
+
try {
|
|
55
|
+
return fs
|
|
56
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
57
|
+
.filter((entry) => entry.isDirectory() && SAFE_NAME.test(entry.name))
|
|
58
|
+
.map((entry) => entry.name);
|
|
59
|
+
} catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Copy into `dir`'s plugin cache every cached version of each enabled plugin
|
|
66
|
+
* that the plugin index does not list, unless a copy is already there.
|
|
67
|
+
*
|
|
68
|
+
* @param {{ dir: string, source: string, enabledPlugins: unknown,
|
|
69
|
+
* indexed: unknown, warnings: string[] }} options
|
|
70
|
+
*/
|
|
71
|
+
export function copyUnindexedPlugins({
|
|
72
|
+
dir,
|
|
73
|
+
source,
|
|
74
|
+
enabledPlugins,
|
|
75
|
+
indexed,
|
|
76
|
+
warnings,
|
|
77
|
+
}) {
|
|
78
|
+
if (!enabledPlugins || typeof enabledPlugins !== 'object') return;
|
|
79
|
+
const listed =
|
|
80
|
+
indexed && typeof indexed === 'object' ? Object.keys(indexed) : [];
|
|
81
|
+
for (const [id, enabled] of Object.entries(enabledPlugins)) {
|
|
82
|
+
if (enabled !== true || listed.includes(id)) continue;
|
|
83
|
+
const parsed = parsePluginId(id);
|
|
84
|
+
if (!parsed) continue;
|
|
85
|
+
const [plugin, marketplace] = parsed;
|
|
86
|
+
const from = path.join(source, 'plugins', 'cache', marketplace, plugin);
|
|
87
|
+
const to = path.join(dir, 'plugins', 'cache', marketplace, plugin);
|
|
88
|
+
// A link here could carry the copy into the user's own cache.
|
|
89
|
+
if (linkOnPath(path.join(dir, 'plugins'), ['cache', marketplace, plugin])) {
|
|
90
|
+
warnings.push(`Skipped plugin ${id} for subc: its cache path is a link.`);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
for (const version of directories(from)) {
|
|
94
|
+
if (fs.existsSync(path.join(to, version))) continue;
|
|
95
|
+
try {
|
|
96
|
+
fs.mkdirSync(to, { recursive: true });
|
|
97
|
+
copyVersion(path.join(from, version), path.join(to, version));
|
|
98
|
+
} catch (error) {
|
|
99
|
+
warnings.push(`Could not copy plugin ${id} for subc: ${error.message}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|