@jgordijn/opencode-remote-config 0.2.0 → 0.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 +45 -0
- package/dist/index.js +301 -163
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -100,6 +100,7 @@ The plugin reads its configuration from a separate JSON file (not `opencode.json
|
|
|
100
100
|
|
|
101
101
|
| Option | Type | Default | Description |
|
|
102
102
|
|--------|------|---------|-------------|
|
|
103
|
+
| `installMethod` | `"link"` or `"copy"` | `"link"` | How to install skills/plugins (symlinks or copies) |
|
|
103
104
|
| `repositories` | Array | `[]` | List of repositories to sync |
|
|
104
105
|
| `repositories[].url` | String | Required | Git URL, HTTPS URL, or `file://` path |
|
|
105
106
|
| `repositories[].ref` | String | Default branch | Branch, tag, or commit SHA (git only) |
|
|
@@ -109,6 +110,50 @@ The plugin reads its configuration from a separate JSON file (not `opencode.json
|
|
|
109
110
|
| `repositories[].plugins` | `"*"` or `{ include: [...] }` or `{ exclude: [...] }` | All plugins | Plugins to import |
|
|
110
111
|
| `repositories[].instructions` | `"*"` or `{ include: [...] }` or `{ exclude: [...] }` | All instructions | Instructions from manifest to import |
|
|
111
112
|
|
|
113
|
+
### Installation Method
|
|
114
|
+
|
|
115
|
+
By default, skills and plugins are installed using symlinks. This is fast and efficient but may not work in all environments.
|
|
116
|
+
|
|
117
|
+
#### Configuration
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"installMethod": "link"
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
| Value | Description |
|
|
126
|
+
|-------|-------------|
|
|
127
|
+
| `link` | Create symlinks (default). Fast, but may not work in containers or on Windows. |
|
|
128
|
+
| `copy` | Copy files. Works everywhere. Uses rsync if available for efficiency. |
|
|
129
|
+
|
|
130
|
+
#### When to Use Copy Mode
|
|
131
|
+
|
|
132
|
+
Use `"installMethod": "copy"` when:
|
|
133
|
+
|
|
134
|
+
- **Dev Containers**: Symlinks break because paths differ between host and container
|
|
135
|
+
- **Windows**: Symlinks require admin privileges or developer mode
|
|
136
|
+
- **Network filesystems**: Some NFS/CIFS mounts don't support symlinks properly
|
|
137
|
+
|
|
138
|
+
#### Example
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"$schema": "https://raw.githubusercontent.com/jgordijn/opencode-remote-config/main/remote-skills.schema.json",
|
|
143
|
+
"installMethod": "copy",
|
|
144
|
+
"repositories": [
|
|
145
|
+
{
|
|
146
|
+
"url": "git@github.com:your-org/shared-skills.git"
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
When using copy mode:
|
|
153
|
+
- rsync is preferred if available (efficient incremental updates)
|
|
154
|
+
- Falls back to Node.js file copy if rsync is unavailable
|
|
155
|
+
- Files are kept in sync with the source repository
|
|
156
|
+
|
|
112
157
|
### Local Directories
|
|
113
158
|
|
|
114
159
|
For development or local skill repositories, use `file://` URLs:
|
package/dist/index.js
CHANGED
|
@@ -7475,8 +7475,49 @@ var coerce = {
|
|
|
7475
7475
|
var NEVER = INVALID;
|
|
7476
7476
|
// src/config.ts
|
|
7477
7477
|
import { existsSync, readFileSync } from "fs";
|
|
7478
|
+
import { join as join2 } from "path";
|
|
7479
|
+
import { homedir as homedir2 } from "os";
|
|
7480
|
+
|
|
7481
|
+
// src/logging.ts
|
|
7482
|
+
import { appendFileSync, mkdirSync } from "fs";
|
|
7478
7483
|
import { join } from "path";
|
|
7479
7484
|
import { homedir } from "os";
|
|
7485
|
+
var DEFAULT_LOG_DIR = join(homedir(), ".cache", "opencode", "remote-config");
|
|
7486
|
+
var currentLogDir = DEFAULT_LOG_DIR;
|
|
7487
|
+
var dirEnsured = false;
|
|
7488
|
+
var LOG_PREFIX = "[remote-config]";
|
|
7489
|
+
function getLogFile() {
|
|
7490
|
+
return join(currentLogDir, "plugin.log");
|
|
7491
|
+
}
|
|
7492
|
+
function timestamp() {
|
|
7493
|
+
return new Date().toISOString();
|
|
7494
|
+
}
|
|
7495
|
+
function writeLog(level, message) {
|
|
7496
|
+
try {
|
|
7497
|
+
if (!dirEnsured) {
|
|
7498
|
+
mkdirSync(currentLogDir, { recursive: true });
|
|
7499
|
+
dirEnsured = true;
|
|
7500
|
+
}
|
|
7501
|
+
appendFileSync(getLogFile(), `${timestamp()} [${level}] ${LOG_PREFIX} ${message}
|
|
7502
|
+
`);
|
|
7503
|
+
} catch {
|
|
7504
|
+
dirEnsured = false;
|
|
7505
|
+
}
|
|
7506
|
+
}
|
|
7507
|
+
function log(message) {
|
|
7508
|
+
writeLog("INFO", message);
|
|
7509
|
+
}
|
|
7510
|
+
function logError(message) {
|
|
7511
|
+
writeLog("ERROR", message);
|
|
7512
|
+
}
|
|
7513
|
+
function logDebug(message) {
|
|
7514
|
+
writeLog("DEBUG", message);
|
|
7515
|
+
}
|
|
7516
|
+
function logWarn(message) {
|
|
7517
|
+
writeLog("WARN", message);
|
|
7518
|
+
}
|
|
7519
|
+
|
|
7520
|
+
// src/config.ts
|
|
7480
7521
|
var CONFIG_FILENAME = "remote-config.json";
|
|
7481
7522
|
var FilterConfigSchema = exports_external.union([
|
|
7482
7523
|
exports_external.object({
|
|
@@ -7514,16 +7555,18 @@ function shouldImport(name, config) {
|
|
|
7514
7555
|
var RemoteSkillsConfigSchema = exports_external.object({
|
|
7515
7556
|
$schema: exports_external.string().optional(),
|
|
7516
7557
|
repositories: exports_external.array(RepositoryConfigSchema).default([]),
|
|
7517
|
-
sync: exports_external.enum(["blocking", "background"]).default("blocking")
|
|
7558
|
+
sync: exports_external.enum(["blocking", "background"]).default("blocking"),
|
|
7559
|
+
installMethod: exports_external.enum(["link", "copy"]).default("link")
|
|
7518
7560
|
}).strict();
|
|
7519
7561
|
var DEFAULT_CONFIG = {
|
|
7520
7562
|
repositories: [],
|
|
7521
|
-
sync: "blocking"
|
|
7563
|
+
sync: "blocking",
|
|
7564
|
+
installMethod: "link"
|
|
7522
7565
|
};
|
|
7523
7566
|
function getConfigPaths() {
|
|
7524
7567
|
return [
|
|
7525
|
-
|
|
7526
|
-
|
|
7568
|
+
join2(process.cwd(), ".opencode", CONFIG_FILENAME),
|
|
7569
|
+
join2(homedir2(), ".config", "opencode", CONFIG_FILENAME)
|
|
7527
7570
|
];
|
|
7528
7571
|
}
|
|
7529
7572
|
function loadConfig() {
|
|
@@ -7538,13 +7581,13 @@ function loadConfigWithLocation() {
|
|
|
7538
7581
|
const parsed = JSON.parse(content);
|
|
7539
7582
|
const result = RemoteSkillsConfigSchema.safeParse(parsed);
|
|
7540
7583
|
if (!result.success) {
|
|
7541
|
-
|
|
7584
|
+
logError(`Invalid configuration in ${configPath}: ${JSON.stringify(result.error.format())}`);
|
|
7542
7585
|
continue;
|
|
7543
7586
|
}
|
|
7544
|
-
const configDir =
|
|
7587
|
+
const configDir = join2(configPath, "..");
|
|
7545
7588
|
return { config: result.data, configDir };
|
|
7546
7589
|
} catch (error) {
|
|
7547
|
-
|
|
7590
|
+
logError(`Error reading ${configPath}: ${error}`);
|
|
7548
7591
|
continue;
|
|
7549
7592
|
}
|
|
7550
7593
|
}
|
|
@@ -7611,11 +7654,11 @@ var CommandConfigSchema = exports_external.object({
|
|
|
7611
7654
|
|
|
7612
7655
|
// src/instruction.ts
|
|
7613
7656
|
import { existsSync as existsSync3 } from "fs";
|
|
7614
|
-
import { join as
|
|
7657
|
+
import { join as join4, normalize, resolve, sep } from "path";
|
|
7615
7658
|
|
|
7616
7659
|
// src/manifest.ts
|
|
7617
7660
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
7618
|
-
import { join as
|
|
7661
|
+
import { join as join3, isAbsolute } from "path";
|
|
7619
7662
|
var MANIFEST_FILENAME = "manifest.json";
|
|
7620
7663
|
function containsPathTraversal(path) {
|
|
7621
7664
|
return path.split("/").some((segment) => segment === ".." || segment === ".");
|
|
@@ -7629,7 +7672,7 @@ var ManifestSchema = exports_external.object({
|
|
|
7629
7672
|
instructions: exports_external.array(instructionPathSchema).optional().default([])
|
|
7630
7673
|
});
|
|
7631
7674
|
function loadManifest(repoPath) {
|
|
7632
|
-
const manifestPath =
|
|
7675
|
+
const manifestPath = join3(repoPath, MANIFEST_FILENAME);
|
|
7633
7676
|
if (!existsSync2(manifestPath)) {
|
|
7634
7677
|
return { status: "not-found" };
|
|
7635
7678
|
}
|
|
@@ -7679,7 +7722,7 @@ function discoverInstructions(repoPath) {
|
|
|
7679
7722
|
console.warn(`[remote-config] Skipping instruction with path traversal: ${instructionName}`);
|
|
7680
7723
|
continue;
|
|
7681
7724
|
}
|
|
7682
|
-
const absolutePath =
|
|
7725
|
+
const absolutePath = join4(repoPath, instructionName);
|
|
7683
7726
|
const resolvedPath = normalize(resolve(absolutePath));
|
|
7684
7727
|
const resolvedRepoPath = normalize(resolve(repoPath));
|
|
7685
7728
|
if (!resolvedPath.startsWith(resolvedRepoPath + sep)) {
|
|
@@ -7835,14 +7878,14 @@ async function discoverAgents(repoPath) {
|
|
|
7835
7878
|
const findAgents = (dir, depth) => {
|
|
7836
7879
|
if (depth > DISCOVERY_LIMITS.maxDepth) {
|
|
7837
7880
|
if (!limitsWarned) {
|
|
7838
|
-
|
|
7881
|
+
logWarn(`Skipping deep directories (max depth: ${DISCOVERY_LIMITS.maxDepth})`);
|
|
7839
7882
|
limitsWarned = true;
|
|
7840
7883
|
}
|
|
7841
7884
|
return;
|
|
7842
7885
|
}
|
|
7843
7886
|
if (filesProcessed >= DISCOVERY_LIMITS.maxFiles) {
|
|
7844
7887
|
if (!limitsWarned) {
|
|
7845
|
-
|
|
7888
|
+
logWarn(`Stopping discovery (max files: ${DISCOVERY_LIMITS.maxFiles})`);
|
|
7846
7889
|
limitsWarned = true;
|
|
7847
7890
|
}
|
|
7848
7891
|
return;
|
|
@@ -7860,7 +7903,7 @@ async function discoverAgents(repoPath) {
|
|
|
7860
7903
|
try {
|
|
7861
7904
|
const stats = fs.statSync(fullPath);
|
|
7862
7905
|
if (stats.size > DISCOVERY_LIMITS.maxFileSize) {
|
|
7863
|
-
|
|
7906
|
+
logWarn(`Skipping large file (${Math.round(stats.size / 1024)}KB): ${entry.name}`);
|
|
7864
7907
|
continue;
|
|
7865
7908
|
}
|
|
7866
7909
|
filesProcessed++;
|
|
@@ -7870,7 +7913,7 @@ async function discoverAgents(repoPath) {
|
|
|
7870
7913
|
agents.push(parsed);
|
|
7871
7914
|
}
|
|
7872
7915
|
} catch (err) {
|
|
7873
|
-
|
|
7916
|
+
logError(`Failed to parse agent ${fullPath}: ${err}`);
|
|
7874
7917
|
}
|
|
7875
7918
|
}
|
|
7876
7919
|
}
|
|
@@ -7896,7 +7939,7 @@ function parseAgentMarkdown(filePath, content, agentDir) {
|
|
|
7896
7939
|
});
|
|
7897
7940
|
} catch (err) {
|
|
7898
7941
|
const relativeToRepo = path.relative(path.dirname(agentDir), filePath);
|
|
7899
|
-
|
|
7942
|
+
logError(`Failed to parse frontmatter in ${relativeToRepo}: ${err}`);
|
|
7900
7943
|
return null;
|
|
7901
7944
|
}
|
|
7902
7945
|
if (!md.data || Object.keys(md.data).length === 0) {
|
|
@@ -7906,7 +7949,7 @@ function parseAgentMarkdown(filePath, content, agentDir) {
|
|
|
7906
7949
|
const agentName = relativePath.replace(/\.md$/i, "");
|
|
7907
7950
|
if (!/^[a-zA-Z0-9_/-]+$/.test(agentName)) {
|
|
7908
7951
|
const relativeToRepo = path.relative(path.dirname(agentDir), filePath);
|
|
7909
|
-
|
|
7952
|
+
logWarn(`Skipping agent with invalid name characters: ${relativeToRepo}`);
|
|
7910
7953
|
return null;
|
|
7911
7954
|
}
|
|
7912
7955
|
const rawConfig = {
|
|
@@ -7915,7 +7958,7 @@ function parseAgentMarkdown(filePath, content, agentDir) {
|
|
|
7915
7958
|
};
|
|
7916
7959
|
const result = AgentConfigSchema.safeParse(rawConfig);
|
|
7917
7960
|
if (!result.success) {
|
|
7918
|
-
|
|
7961
|
+
logError(`Invalid agent config in ${filePath}: ${JSON.stringify(result.error.format())}`);
|
|
7919
7962
|
return null;
|
|
7920
7963
|
}
|
|
7921
7964
|
return {
|
|
@@ -7938,14 +7981,14 @@ async function discoverCommands(repoPath) {
|
|
|
7938
7981
|
const findCommands = (dir, depth) => {
|
|
7939
7982
|
if (depth > DISCOVERY_LIMITS.maxDepth) {
|
|
7940
7983
|
if (!limitsWarned) {
|
|
7941
|
-
|
|
7984
|
+
logWarn(`Skipping deep directories (max depth: ${DISCOVERY_LIMITS.maxDepth})`);
|
|
7942
7985
|
limitsWarned = true;
|
|
7943
7986
|
}
|
|
7944
7987
|
return;
|
|
7945
7988
|
}
|
|
7946
7989
|
if (filesProcessed >= DISCOVERY_LIMITS.maxFiles) {
|
|
7947
7990
|
if (!limitsWarned) {
|
|
7948
|
-
|
|
7991
|
+
logWarn(`Stopping discovery (max files: ${DISCOVERY_LIMITS.maxFiles})`);
|
|
7949
7992
|
limitsWarned = true;
|
|
7950
7993
|
}
|
|
7951
7994
|
return;
|
|
@@ -7963,7 +8006,7 @@ async function discoverCommands(repoPath) {
|
|
|
7963
8006
|
try {
|
|
7964
8007
|
const stats = fs.statSync(fullPath);
|
|
7965
8008
|
if (stats.size > DISCOVERY_LIMITS.maxFileSize) {
|
|
7966
|
-
|
|
8009
|
+
logWarn(`Skipping large file (${Math.round(stats.size / 1024)}KB): ${entry.name}`);
|
|
7967
8010
|
continue;
|
|
7968
8011
|
}
|
|
7969
8012
|
filesProcessed++;
|
|
@@ -7973,7 +8016,7 @@ async function discoverCommands(repoPath) {
|
|
|
7973
8016
|
commands.push(parsed);
|
|
7974
8017
|
}
|
|
7975
8018
|
} catch (err) {
|
|
7976
|
-
|
|
8019
|
+
logError(`Failed to parse command ${fullPath}: ${err}`);
|
|
7977
8020
|
}
|
|
7978
8021
|
}
|
|
7979
8022
|
}
|
|
@@ -7999,14 +8042,14 @@ function parseCommandMarkdown(filePath, content, commandDir) {
|
|
|
7999
8042
|
});
|
|
8000
8043
|
} catch (err) {
|
|
8001
8044
|
const relativeToRepo = path.relative(path.dirname(commandDir), filePath);
|
|
8002
|
-
|
|
8045
|
+
logError(`Failed to parse frontmatter in ${relativeToRepo}: ${err}`);
|
|
8003
8046
|
return null;
|
|
8004
8047
|
}
|
|
8005
8048
|
const relativePath = path.relative(commandDir, filePath).replace(/\\/g, "/");
|
|
8006
8049
|
const commandName = relativePath.replace(/\.md$/i, "");
|
|
8007
8050
|
if (!/^[a-zA-Z0-9_/-]+$/.test(commandName)) {
|
|
8008
8051
|
const relativeToRepo = path.relative(path.dirname(commandDir), filePath);
|
|
8009
|
-
|
|
8052
|
+
logWarn(`Skipping command with invalid name characters: ${relativeToRepo}`);
|
|
8010
8053
|
return null;
|
|
8011
8054
|
}
|
|
8012
8055
|
const rawConfig = {
|
|
@@ -8015,7 +8058,7 @@ function parseCommandMarkdown(filePath, content, commandDir) {
|
|
|
8015
8058
|
};
|
|
8016
8059
|
const result = CommandConfigSchema.safeParse(rawConfig);
|
|
8017
8060
|
if (!result.success) {
|
|
8018
|
-
|
|
8061
|
+
logError(`Invalid command config in ${filePath}: ${JSON.stringify(result.error.format())}`);
|
|
8019
8062
|
return null;
|
|
8020
8063
|
}
|
|
8021
8064
|
return {
|
|
@@ -8038,14 +8081,14 @@ async function discoverPlugins(repoPath, repoShortName) {
|
|
|
8038
8081
|
const findPlugins = (dir, depth) => {
|
|
8039
8082
|
if (depth > DISCOVERY_LIMITS.maxDepth) {
|
|
8040
8083
|
if (!limitsWarned) {
|
|
8041
|
-
|
|
8084
|
+
logWarn(`Skipping deep directories (max depth: ${DISCOVERY_LIMITS.maxDepth})`);
|
|
8042
8085
|
limitsWarned = true;
|
|
8043
8086
|
}
|
|
8044
8087
|
return;
|
|
8045
8088
|
}
|
|
8046
8089
|
if (filesProcessed >= DISCOVERY_LIMITS.maxFiles) {
|
|
8047
8090
|
if (!limitsWarned) {
|
|
8048
|
-
|
|
8091
|
+
logWarn(`Stopping discovery (max files: ${DISCOVERY_LIMITS.maxFiles})`);
|
|
8049
8092
|
limitsWarned = true;
|
|
8050
8093
|
}
|
|
8051
8094
|
return;
|
|
@@ -8063,7 +8106,7 @@ async function discoverPlugins(repoPath, repoShortName) {
|
|
|
8063
8106
|
try {
|
|
8064
8107
|
const stats = fs.statSync(fullPath);
|
|
8065
8108
|
if (stats.size > DISCOVERY_LIMITS.maxFileSize) {
|
|
8066
|
-
|
|
8109
|
+
logWarn(`Skipping large file (${Math.round(stats.size / 1024)}KB): ${entry.name}`);
|
|
8067
8110
|
continue;
|
|
8068
8111
|
}
|
|
8069
8112
|
filesProcessed++;
|
|
@@ -8072,7 +8115,7 @@ async function discoverPlugins(repoPath, repoShortName) {
|
|
|
8072
8115
|
const pluginName = relativePath.slice(0, -ext.length).replace(/\//g, "-");
|
|
8073
8116
|
if (!/^[a-zA-Z0-9_-]+$/.test(pluginName)) {
|
|
8074
8117
|
const relativeToRepo = path.relative(path.dirname(pluginDir), fullPath);
|
|
8075
|
-
|
|
8118
|
+
logWarn(`Skipping plugin with invalid name characters: ${relativeToRepo}`);
|
|
8076
8119
|
continue;
|
|
8077
8120
|
}
|
|
8078
8121
|
plugins.push({
|
|
@@ -8082,7 +8125,7 @@ async function discoverPlugins(repoPath, repoShortName) {
|
|
|
8082
8125
|
extension: ext
|
|
8083
8126
|
});
|
|
8084
8127
|
} catch (err) {
|
|
8085
|
-
|
|
8128
|
+
logError(`Failed to process plugin ${fullPath}: ${err}`);
|
|
8086
8129
|
}
|
|
8087
8130
|
}
|
|
8088
8131
|
}
|
|
@@ -8197,20 +8240,122 @@ async function syncRepositories(configs) {
|
|
|
8197
8240
|
return results;
|
|
8198
8241
|
}
|
|
8199
8242
|
|
|
8200
|
-
// src/
|
|
8201
|
-
import * as
|
|
8202
|
-
import * as
|
|
8243
|
+
// src/install.ts
|
|
8244
|
+
import * as path3 from "path";
|
|
8245
|
+
import * as fs3 from "fs";
|
|
8246
|
+
var {$: $3 } = globalThis.Bun;
|
|
8247
|
+
|
|
8248
|
+
// src/copy.ts
|
|
8203
8249
|
var {$: $2 } = globalThis.Bun;
|
|
8204
|
-
|
|
8205
|
-
|
|
8206
|
-
|
|
8207
|
-
|
|
8250
|
+
import * as fs2 from "fs";
|
|
8251
|
+
import * as path2 from "path";
|
|
8252
|
+
var rsyncAvailable = null;
|
|
8253
|
+
async function detectRsync() {
|
|
8254
|
+
if (rsyncAvailable !== null) {
|
|
8255
|
+
return rsyncAvailable;
|
|
8256
|
+
}
|
|
8257
|
+
try {
|
|
8258
|
+
const result = await $2`which rsync`.quiet();
|
|
8259
|
+
rsyncAvailable = result.exitCode === 0;
|
|
8260
|
+
} catch {
|
|
8261
|
+
rsyncAvailable = false;
|
|
8262
|
+
}
|
|
8263
|
+
return rsyncAvailable;
|
|
8264
|
+
}
|
|
8265
|
+
async function copyWithRsync(source, target) {
|
|
8266
|
+
fs2.mkdirSync(path2.dirname(target), { recursive: true });
|
|
8267
|
+
const result = await $2`rsync -a --delete ${source}/ ${target}/`.quiet();
|
|
8268
|
+
if (result.exitCode !== 0) {
|
|
8269
|
+
throw new Error(`rsync failed: ${result.stderr.toString()}`);
|
|
8270
|
+
}
|
|
8271
|
+
}
|
|
8272
|
+
function copyWithNodeFs(source, target) {
|
|
8273
|
+
if (fs2.existsSync(target)) {
|
|
8274
|
+
const stat = fs2.lstatSync(target);
|
|
8275
|
+
if (stat.isSymbolicLink() || stat.isFile()) {
|
|
8276
|
+
fs2.unlinkSync(target);
|
|
8277
|
+
} else {
|
|
8278
|
+
fs2.rmSync(target, { recursive: true, force: true });
|
|
8279
|
+
}
|
|
8280
|
+
}
|
|
8281
|
+
fs2.mkdirSync(path2.dirname(target), { recursive: true });
|
|
8282
|
+
fs2.cpSync(source, target, { recursive: true });
|
|
8283
|
+
}
|
|
8284
|
+
async function syncDirectory(source, target) {
|
|
8285
|
+
let sourceStat;
|
|
8286
|
+
try {
|
|
8287
|
+
sourceStat = fs2.statSync(source);
|
|
8288
|
+
} catch {
|
|
8289
|
+
throw new Error(`Source does not exist: ${source}`);
|
|
8290
|
+
}
|
|
8291
|
+
if (!sourceStat.isDirectory()) {
|
|
8292
|
+
throw new Error(`Source is not a directory: ${source}`);
|
|
8293
|
+
}
|
|
8294
|
+
const resolvedSource = path2.resolve(source);
|
|
8295
|
+
const resolvedTarget = path2.resolve(target);
|
|
8296
|
+
if (resolvedTarget.startsWith(resolvedSource + path2.sep)) {
|
|
8297
|
+
throw new Error(`Target cannot be inside source: ${target} is inside ${source}`);
|
|
8298
|
+
}
|
|
8299
|
+
if (resolvedSource.startsWith(resolvedTarget + path2.sep)) {
|
|
8300
|
+
throw new Error(`Source cannot be inside target: ${source} is inside ${target}`);
|
|
8301
|
+
}
|
|
8302
|
+
const hasRsync = await detectRsync();
|
|
8303
|
+
if (hasRsync) {
|
|
8304
|
+
try {
|
|
8305
|
+
await copyWithRsync(source, target);
|
|
8306
|
+
logDebug(`Copied ${source} \u2192 ${target} using rsync`);
|
|
8307
|
+
return { method: "rsync" };
|
|
8308
|
+
} catch (err) {
|
|
8309
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
8310
|
+
logError(`rsync failed, falling back to fs: ${errorMessage}`);
|
|
8311
|
+
}
|
|
8312
|
+
}
|
|
8313
|
+
try {
|
|
8314
|
+
copyWithNodeFs(source, target);
|
|
8315
|
+
logDebug(`Copied ${source} \u2192 ${target} using fs.cpSync`);
|
|
8316
|
+
return { method: "fs" };
|
|
8317
|
+
} catch (err) {
|
|
8318
|
+
try {
|
|
8319
|
+
if (fs2.existsSync(target)) {
|
|
8320
|
+
const stat = fs2.lstatSync(target);
|
|
8321
|
+
if (stat.isSymbolicLink() || stat.isFile()) {
|
|
8322
|
+
fs2.unlinkSync(target);
|
|
8323
|
+
} else {
|
|
8324
|
+
fs2.rmSync(target, { recursive: true, force: true });
|
|
8325
|
+
}
|
|
8326
|
+
}
|
|
8327
|
+
} catch {}
|
|
8328
|
+
throw err;
|
|
8329
|
+
}
|
|
8330
|
+
}
|
|
8331
|
+
|
|
8332
|
+
// src/install.ts
|
|
8333
|
+
var SKILL_BASE = path3.join(process.env.HOME || "~", ".config", "opencode", "skill");
|
|
8334
|
+
var PLUGINS_DIR = path3.join(SKILL_BASE, "_plugins");
|
|
8335
|
+
function getInstallPath(repoShortName, skillName) {
|
|
8336
|
+
return path3.join(PLUGINS_DIR, repoShortName, skillName);
|
|
8208
8337
|
}
|
|
8209
8338
|
function ensurePluginsDir() {
|
|
8210
|
-
|
|
8339
|
+
fs3.mkdirSync(PLUGINS_DIR, { recursive: true });
|
|
8340
|
+
}
|
|
8341
|
+
function createSymlinkSync(sourcePath, targetPath) {
|
|
8342
|
+
if (fs3.existsSync(targetPath)) {
|
|
8343
|
+
const stats = fs3.lstatSync(targetPath);
|
|
8344
|
+
if (stats.isSymbolicLink()) {
|
|
8345
|
+
const existingTarget = fs3.readlinkSync(targetPath);
|
|
8346
|
+
if (existingTarget === sourcePath) {
|
|
8347
|
+
return { created: false };
|
|
8348
|
+
}
|
|
8349
|
+
fs3.unlinkSync(targetPath);
|
|
8350
|
+
} else {
|
|
8351
|
+
return { created: false, error: `Path exists and is not a symlink: ${targetPath}` };
|
|
8352
|
+
}
|
|
8353
|
+
}
|
|
8354
|
+
fs3.symlinkSync(sourcePath, targetPath, "dir");
|
|
8355
|
+
return { created: true };
|
|
8211
8356
|
}
|
|
8212
|
-
function
|
|
8213
|
-
const targetPath =
|
|
8357
|
+
async function createSkillInstall(skill, repoShortName, installMethod = "link") {
|
|
8358
|
+
const targetPath = getInstallPath(repoShortName, skill.name);
|
|
8214
8359
|
const result = {
|
|
8215
8360
|
skillName: skill.name,
|
|
8216
8361
|
sourcePath: skill.path,
|
|
@@ -8218,78 +8363,81 @@ function createSkillSymlink(skill, repoShortName) {
|
|
|
8218
8363
|
created: false
|
|
8219
8364
|
};
|
|
8220
8365
|
try {
|
|
8221
|
-
|
|
8222
|
-
if (
|
|
8223
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
} else {
|
|
8231
|
-
result.error = `Path exists and is not a symlink: ${targetPath}`;
|
|
8232
|
-
return result;
|
|
8366
|
+
fs3.mkdirSync(path3.dirname(targetPath), { recursive: true });
|
|
8367
|
+
if (installMethod === "copy") {
|
|
8368
|
+
await syncDirectory(skill.path, targetPath);
|
|
8369
|
+
result.created = true;
|
|
8370
|
+
} else {
|
|
8371
|
+
const symlinkResult = createSymlinkSync(skill.path, targetPath);
|
|
8372
|
+
result.created = symlinkResult.created;
|
|
8373
|
+
if (symlinkResult.error) {
|
|
8374
|
+
result.error = symlinkResult.error;
|
|
8233
8375
|
}
|
|
8234
8376
|
}
|
|
8235
|
-
fs2.symlinkSync(skill.path, targetPath, "dir");
|
|
8236
|
-
result.created = true;
|
|
8237
8377
|
} catch (err) {
|
|
8238
8378
|
result.error = err instanceof Error ? err.message : String(err);
|
|
8239
8379
|
}
|
|
8240
8380
|
return result;
|
|
8241
8381
|
}
|
|
8242
|
-
function
|
|
8382
|
+
async function createInstallsForRepo(syncResult, installMethod = "link") {
|
|
8243
8383
|
ensurePluginsDir();
|
|
8244
8384
|
const results = [];
|
|
8245
8385
|
for (const skill of syncResult.skills) {
|
|
8246
|
-
const result =
|
|
8386
|
+
const result = await createSkillInstall(skill, syncResult.shortName, installMethod);
|
|
8247
8387
|
results.push(result);
|
|
8248
8388
|
}
|
|
8249
8389
|
return results;
|
|
8250
8390
|
}
|
|
8251
|
-
function
|
|
8252
|
-
const
|
|
8253
|
-
if (!
|
|
8254
|
-
return
|
|
8255
|
-
}
|
|
8256
|
-
const
|
|
8257
|
-
|
|
8258
|
-
|
|
8259
|
-
|
|
8391
|
+
function getExistingInstalls() {
|
|
8392
|
+
const installs = new Map;
|
|
8393
|
+
if (!fs3.existsSync(PLUGINS_DIR)) {
|
|
8394
|
+
return installs;
|
|
8395
|
+
}
|
|
8396
|
+
const repoEntries = fs3.readdirSync(PLUGINS_DIR, { withFileTypes: true });
|
|
8397
|
+
for (const repoEntry of repoEntries) {
|
|
8398
|
+
if (repoEntry.name.startsWith(".") || !repoEntry.isDirectory())
|
|
8399
|
+
continue;
|
|
8400
|
+
const repoPath = path3.join(PLUGINS_DIR, repoEntry.name);
|
|
8401
|
+
const skillEntries = fs3.readdirSync(repoPath, { withFileTypes: true });
|
|
8402
|
+
for (const skillEntry of skillEntries) {
|
|
8403
|
+
if (skillEntry.name.startsWith("."))
|
|
8260
8404
|
continue;
|
|
8261
|
-
const fullPath =
|
|
8262
|
-
const relativePath =
|
|
8263
|
-
if (
|
|
8264
|
-
const target =
|
|
8265
|
-
|
|
8266
|
-
} else if (
|
|
8267
|
-
|
|
8405
|
+
const fullPath = path3.join(repoPath, skillEntry.name);
|
|
8406
|
+
const relativePath = `${repoEntry.name}/${skillEntry.name}`;
|
|
8407
|
+
if (skillEntry.isSymbolicLink()) {
|
|
8408
|
+
const target = fs3.readlinkSync(fullPath);
|
|
8409
|
+
installs.set(relativePath, target);
|
|
8410
|
+
} else if (skillEntry.isDirectory()) {
|
|
8411
|
+
installs.set(relativePath, fullPath);
|
|
8268
8412
|
}
|
|
8269
8413
|
}
|
|
8270
|
-
}
|
|
8271
|
-
|
|
8272
|
-
return symlinks;
|
|
8414
|
+
}
|
|
8415
|
+
return installs;
|
|
8273
8416
|
}
|
|
8274
|
-
function
|
|
8417
|
+
function cleanupStaleInstalls(currentSkills) {
|
|
8275
8418
|
const result = {
|
|
8276
8419
|
removed: [],
|
|
8277
8420
|
errors: []
|
|
8278
8421
|
};
|
|
8279
|
-
const
|
|
8280
|
-
for (const [relativePath] of
|
|
8422
|
+
const existingInstalls = getExistingInstalls();
|
|
8423
|
+
for (const [relativePath] of existingInstalls) {
|
|
8281
8424
|
if (!currentSkills.has(relativePath)) {
|
|
8282
|
-
const fullPath =
|
|
8425
|
+
const fullPath = path3.join(PLUGINS_DIR, relativePath);
|
|
8283
8426
|
try {
|
|
8284
|
-
|
|
8427
|
+
const stats = fs3.lstatSync(fullPath);
|
|
8428
|
+
if (stats.isSymbolicLink()) {
|
|
8429
|
+
fs3.unlinkSync(fullPath);
|
|
8430
|
+
} else if (stats.isDirectory()) {
|
|
8431
|
+
fs3.rmSync(fullPath, { recursive: true, force: true });
|
|
8432
|
+
}
|
|
8285
8433
|
result.removed.push(relativePath);
|
|
8286
|
-
let parentDir =
|
|
8434
|
+
let parentDir = path3.dirname(fullPath);
|
|
8287
8435
|
while (parentDir !== PLUGINS_DIR && parentDir.startsWith(PLUGINS_DIR)) {
|
|
8288
8436
|
try {
|
|
8289
|
-
const entries =
|
|
8437
|
+
const entries = fs3.readdirSync(parentDir);
|
|
8290
8438
|
if (entries.length === 0) {
|
|
8291
|
-
|
|
8292
|
-
parentDir =
|
|
8439
|
+
fs3.rmdirSync(parentDir);
|
|
8440
|
+
parentDir = path3.dirname(parentDir);
|
|
8293
8441
|
} else {
|
|
8294
8442
|
break;
|
|
8295
8443
|
}
|
|
@@ -8305,16 +8453,16 @@ function cleanupStaleSymlinks(currentSkills) {
|
|
|
8305
8453
|
return result;
|
|
8306
8454
|
}
|
|
8307
8455
|
function hasLocalConflict(skillName) {
|
|
8308
|
-
const localSkillPath =
|
|
8309
|
-
if (
|
|
8310
|
-
const realPath =
|
|
8456
|
+
const localSkillPath = path3.join(SKILL_BASE, skillName);
|
|
8457
|
+
if (fs3.existsSync(localSkillPath)) {
|
|
8458
|
+
const realPath = fs3.realpathSync(localSkillPath);
|
|
8311
8459
|
return !realPath.includes("_plugins");
|
|
8312
8460
|
}
|
|
8313
8461
|
return false;
|
|
8314
8462
|
}
|
|
8315
8463
|
async function findGitRoot(startPath) {
|
|
8316
8464
|
try {
|
|
8317
|
-
const result = await $
|
|
8465
|
+
const result = await $3`git -C ${startPath} rev-parse --show-toplevel`.quiet();
|
|
8318
8466
|
if (result.exitCode === 0) {
|
|
8319
8467
|
return result.stdout.toString().trim();
|
|
8320
8468
|
}
|
|
@@ -8326,14 +8474,14 @@ async function ensureGitignore() {
|
|
|
8326
8474
|
if (!gitRoot) {
|
|
8327
8475
|
return { modified: false, gitRoot: null };
|
|
8328
8476
|
}
|
|
8329
|
-
const gitignorePath =
|
|
8477
|
+
const gitignorePath = path3.join(gitRoot, ".gitignore");
|
|
8330
8478
|
const pluginsEntry = "_plugins/";
|
|
8331
|
-
const relativePath =
|
|
8479
|
+
const relativePath = path3.relative(gitRoot, PLUGINS_DIR);
|
|
8332
8480
|
const gitignoreEntry = relativePath ? `${relativePath}/` : pluginsEntry;
|
|
8333
8481
|
let content = "";
|
|
8334
8482
|
let exists = false;
|
|
8335
8483
|
try {
|
|
8336
|
-
content =
|
|
8484
|
+
content = fs3.readFileSync(gitignorePath, "utf-8");
|
|
8337
8485
|
exists = true;
|
|
8338
8486
|
} catch {}
|
|
8339
8487
|
const lines = content.split(`
|
|
@@ -8355,27 +8503,41 @@ ${gitignoreEntry}
|
|
|
8355
8503
|
` : `# OpenCode remote skills plugin
|
|
8356
8504
|
${gitignoreEntry}
|
|
8357
8505
|
`;
|
|
8358
|
-
|
|
8506
|
+
fs3.writeFileSync(gitignorePath, newContent);
|
|
8359
8507
|
return { modified: true, gitRoot };
|
|
8360
8508
|
}
|
|
8361
8509
|
|
|
8362
|
-
// src/plugin-
|
|
8363
|
-
import * as
|
|
8364
|
-
import * as
|
|
8365
|
-
import { homedir as
|
|
8366
|
-
var DEFAULT_PLUGIN_DIR =
|
|
8510
|
+
// src/plugin-install.ts
|
|
8511
|
+
import * as fs4 from "fs";
|
|
8512
|
+
import * as path4 from "path";
|
|
8513
|
+
import { homedir as homedir3 } from "os";
|
|
8514
|
+
var DEFAULT_PLUGIN_DIR = path4.join(homedir3(), ".config", "opencode", "plugin");
|
|
8367
8515
|
var REMOTE_PREFIX = "_remote_";
|
|
8516
|
+
function removePathIfExists(targetPath) {
|
|
8517
|
+
try {
|
|
8518
|
+
const stats = fs4.lstatSync(targetPath);
|
|
8519
|
+
if (stats.isDirectory()) {
|
|
8520
|
+
fs4.rmSync(targetPath, { recursive: true });
|
|
8521
|
+
} else {
|
|
8522
|
+
fs4.unlinkSync(targetPath);
|
|
8523
|
+
}
|
|
8524
|
+
} catch (err) {
|
|
8525
|
+
if (err.code !== "ENOENT") {
|
|
8526
|
+
throw err;
|
|
8527
|
+
}
|
|
8528
|
+
}
|
|
8529
|
+
}
|
|
8368
8530
|
function getPluginSymlinkName(plugin) {
|
|
8369
8531
|
return `${REMOTE_PREFIX}${plugin.repoShortName}_${plugin.name}${plugin.extension}`;
|
|
8370
8532
|
}
|
|
8371
8533
|
function ensurePluginDir(pluginDir = DEFAULT_PLUGIN_DIR) {
|
|
8372
|
-
if (!
|
|
8373
|
-
|
|
8534
|
+
if (!fs4.existsSync(pluginDir)) {
|
|
8535
|
+
fs4.mkdirSync(pluginDir, { recursive: true });
|
|
8374
8536
|
}
|
|
8375
8537
|
}
|
|
8376
|
-
function
|
|
8538
|
+
function createPluginInstall(plugin, pluginDir = DEFAULT_PLUGIN_DIR, installMethod = "link") {
|
|
8377
8539
|
const symlinkName = getPluginSymlinkName(plugin);
|
|
8378
|
-
const symlinkPath =
|
|
8540
|
+
const symlinkPath = path4.join(pluginDir, symlinkName);
|
|
8379
8541
|
const result = {
|
|
8380
8542
|
pluginName: plugin.name,
|
|
8381
8543
|
symlinkName,
|
|
@@ -8384,45 +8546,42 @@ function createPluginSymlink(plugin, pluginDir = DEFAULT_PLUGIN_DIR) {
|
|
|
8384
8546
|
};
|
|
8385
8547
|
try {
|
|
8386
8548
|
ensurePluginDir(pluginDir);
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
}
|
|
8391
|
-
|
|
8392
|
-
throw err;
|
|
8393
|
-
}
|
|
8549
|
+
removePathIfExists(symlinkPath);
|
|
8550
|
+
if (installMethod === "copy") {
|
|
8551
|
+
fs4.cpSync(plugin.path, symlinkPath);
|
|
8552
|
+
} else {
|
|
8553
|
+
fs4.symlinkSync(plugin.path, symlinkPath);
|
|
8394
8554
|
}
|
|
8395
|
-
fs3.symlinkSync(plugin.path, symlinkPath);
|
|
8396
8555
|
} catch (err) {
|
|
8397
8556
|
result.error = err instanceof Error ? err.message : String(err);
|
|
8398
8557
|
}
|
|
8399
8558
|
return result;
|
|
8400
8559
|
}
|
|
8401
|
-
function
|
|
8402
|
-
return plugins.map((
|
|
8560
|
+
function createPluginInstalls(plugins, pluginDir = DEFAULT_PLUGIN_DIR, installMethod = "link") {
|
|
8561
|
+
return plugins.map((plugin) => createPluginInstall(plugin, pluginDir, installMethod));
|
|
8403
8562
|
}
|
|
8404
|
-
function
|
|
8405
|
-
if (!
|
|
8563
|
+
function getRemotePluginInstalls(pluginDir = DEFAULT_PLUGIN_DIR) {
|
|
8564
|
+
if (!fs4.existsSync(pluginDir)) {
|
|
8406
8565
|
return [];
|
|
8407
8566
|
}
|
|
8408
8567
|
try {
|
|
8409
|
-
const entries =
|
|
8568
|
+
const entries = fs4.readdirSync(pluginDir);
|
|
8410
8569
|
return entries.filter((name) => name.startsWith(REMOTE_PREFIX));
|
|
8411
8570
|
} catch {
|
|
8412
8571
|
return [];
|
|
8413
8572
|
}
|
|
8414
8573
|
}
|
|
8415
|
-
function
|
|
8574
|
+
function cleanupStalePluginInstalls(currentInstalls, pluginDir = DEFAULT_PLUGIN_DIR) {
|
|
8416
8575
|
const result = {
|
|
8417
8576
|
removed: [],
|
|
8418
8577
|
errors: []
|
|
8419
8578
|
};
|
|
8420
|
-
const existing =
|
|
8579
|
+
const existing = getRemotePluginInstalls(pluginDir);
|
|
8421
8580
|
for (const name of existing) {
|
|
8422
|
-
if (!
|
|
8423
|
-
const
|
|
8581
|
+
if (!currentInstalls.has(name)) {
|
|
8582
|
+
const installPath = path4.join(pluginDir, name);
|
|
8424
8583
|
try {
|
|
8425
|
-
|
|
8584
|
+
removePathIfExists(installPath);
|
|
8426
8585
|
result.removed.push(name);
|
|
8427
8586
|
} catch (err) {
|
|
8428
8587
|
result.errors.push(`Failed to remove ${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -8433,33 +8592,7 @@ function cleanupStalePluginSymlinks(currentSymlinks, pluginDir = DEFAULT_PLUGIN_
|
|
|
8433
8592
|
}
|
|
8434
8593
|
|
|
8435
8594
|
// src/index.ts
|
|
8436
|
-
import { appendFileSync, mkdirSync as mkdirSync4 } from "fs";
|
|
8437
|
-
import { join as join7 } from "path";
|
|
8438
|
-
import { homedir as homedir3 } from "os";
|
|
8439
|
-
var LOG_PREFIX = "[remote-config]";
|
|
8440
|
-
var LOG_DIR = join7(homedir3(), ".cache", "opencode", "remote-config");
|
|
8441
|
-
var LOG_FILE = join7(LOG_DIR, "plugin.log");
|
|
8442
8595
|
var initialized = false;
|
|
8443
|
-
function timestamp() {
|
|
8444
|
-
return new Date().toISOString();
|
|
8445
|
-
}
|
|
8446
|
-
function writeLog(level, message) {
|
|
8447
|
-
try {
|
|
8448
|
-
mkdirSync4(LOG_DIR, { recursive: true });
|
|
8449
|
-
appendFileSync(LOG_FILE, `${timestamp()} [${level}] ${message}
|
|
8450
|
-
`);
|
|
8451
|
-
} catch {}
|
|
8452
|
-
}
|
|
8453
|
-
function log(message) {
|
|
8454
|
-
const fullMessage = `${LOG_PREFIX} ${message}`;
|
|
8455
|
-
console.log(fullMessage);
|
|
8456
|
-
writeLog("INFO", message);
|
|
8457
|
-
}
|
|
8458
|
-
function logError(message) {
|
|
8459
|
-
const fullMessage = `${LOG_PREFIX} ${message}`;
|
|
8460
|
-
console.error(fullMessage);
|
|
8461
|
-
writeLog("ERROR", message);
|
|
8462
|
-
}
|
|
8463
8596
|
async function performSync(config) {
|
|
8464
8597
|
if (config.repositories.length === 0) {
|
|
8465
8598
|
return { results: [], skippedConflicts: [], totalSkills: 0, remoteAgents: new Map, remoteCommands: new Map, remoteInstructions: [], pluginsChanged: false, totalPlugins: 0 };
|
|
@@ -8474,30 +8607,30 @@ async function performSync(config) {
|
|
|
8474
8607
|
logError(`\u2717 Failed to sync ${result.shortName}: ${result.error}`);
|
|
8475
8608
|
continue;
|
|
8476
8609
|
}
|
|
8477
|
-
const
|
|
8610
|
+
const skillsToInstall = result.skills.filter((skill) => {
|
|
8478
8611
|
if (hasLocalConflict(skill.name)) {
|
|
8479
8612
|
skippedConflicts.push(skill.name);
|
|
8480
8613
|
return false;
|
|
8481
8614
|
}
|
|
8482
8615
|
return true;
|
|
8483
8616
|
});
|
|
8484
|
-
const filteredResult = { ...result, skills:
|
|
8485
|
-
const
|
|
8486
|
-
for (const sr of
|
|
8617
|
+
const filteredResult = { ...result, skills: skillsToInstall };
|
|
8618
|
+
const installResults = await createInstallsForRepo(filteredResult, config.installMethod);
|
|
8619
|
+
for (const sr of installResults) {
|
|
8487
8620
|
if (!sr.error) {
|
|
8488
8621
|
currentSkills.add(`${result.shortName}/${sr.skillName}`);
|
|
8489
8622
|
totalSkills++;
|
|
8490
8623
|
} else {
|
|
8491
|
-
logError(`\u2717 Failed to
|
|
8624
|
+
logError(`\u2717 Failed to install skill ${sr.skillName}: ${sr.error}`);
|
|
8492
8625
|
}
|
|
8493
8626
|
}
|
|
8494
|
-
const skillCount =
|
|
8627
|
+
const skillCount = skillsToInstall.length;
|
|
8495
8628
|
const status = result.updated ? "\u2713" : "\u2713";
|
|
8496
8629
|
log(`${status} ${result.shortName} (${result.ref}) - ${skillCount} skills`);
|
|
8497
8630
|
}
|
|
8498
|
-
const cleanup =
|
|
8631
|
+
const cleanup = cleanupStaleInstalls(currentSkills);
|
|
8499
8632
|
if (cleanup.removed.length > 0) {
|
|
8500
|
-
log(`Cleaned up ${cleanup.removed.length} stale
|
|
8633
|
+
log(`Cleaned up ${cleanup.removed.length} stale skill installs`);
|
|
8501
8634
|
}
|
|
8502
8635
|
for (const conflict of skippedConflicts) {
|
|
8503
8636
|
log(`\u26A0 Conflict: '${conflict}' exists locally, skipping`);
|
|
@@ -8557,25 +8690,25 @@ async function performSync(config) {
|
|
|
8557
8690
|
continue;
|
|
8558
8691
|
allPlugins.push(...result.plugins);
|
|
8559
8692
|
}
|
|
8560
|
-
const
|
|
8561
|
-
const
|
|
8693
|
+
const existingPluginInstalls = new Set(getRemotePluginInstalls());
|
|
8694
|
+
const newPluginInstalls = new Set;
|
|
8562
8695
|
if (allPlugins.length > 0) {
|
|
8563
|
-
const
|
|
8564
|
-
for (const sr of
|
|
8696
|
+
const installResults = createPluginInstalls(allPlugins, undefined, config.installMethod);
|
|
8697
|
+
for (const sr of installResults) {
|
|
8565
8698
|
if (!sr.error) {
|
|
8566
|
-
|
|
8699
|
+
newPluginInstalls.add(sr.symlinkName);
|
|
8567
8700
|
} else {
|
|
8568
|
-
logError(`\u2717 Failed to
|
|
8701
|
+
logError(`\u2717 Failed to install plugin ${sr.pluginName}: ${sr.error}`);
|
|
8569
8702
|
}
|
|
8570
8703
|
}
|
|
8571
8704
|
log(`Discovered ${allPlugins.length} remote plugins`);
|
|
8572
8705
|
}
|
|
8573
|
-
const pluginCleanup =
|
|
8706
|
+
const pluginCleanup = cleanupStalePluginInstalls(newPluginInstalls);
|
|
8574
8707
|
if (pluginCleanup.removed.length > 0) {
|
|
8575
|
-
log(`Cleaned up ${pluginCleanup.removed.length} stale plugin
|
|
8708
|
+
log(`Cleaned up ${pluginCleanup.removed.length} stale plugin installs`);
|
|
8576
8709
|
}
|
|
8577
|
-
const pluginsChanged = !setsEqual(
|
|
8578
|
-
const totalPlugins =
|
|
8710
|
+
const pluginsChanged = !setsEqual(existingPluginInstalls, newPluginInstalls);
|
|
8711
|
+
const totalPlugins = newPluginInstalls.size;
|
|
8579
8712
|
return { results, skippedConflicts, totalSkills, remoteAgents, remoteCommands, remoteInstructions, pluginsChanged, totalPlugins };
|
|
8580
8713
|
}
|
|
8581
8714
|
function setsEqual(a, b) {
|
|
@@ -8596,6 +8729,11 @@ var RemoteSkillsPlugin = async (ctx) => {
|
|
|
8596
8729
|
if (pluginConfig.repositories.length === 0) {
|
|
8597
8730
|
return {};
|
|
8598
8731
|
}
|
|
8732
|
+
if (pluginConfig.installMethod === "copy") {
|
|
8733
|
+
logDebug("Using copy mode for skill/plugin installation");
|
|
8734
|
+
} else {
|
|
8735
|
+
logDebug("Using symlink mode for skill/plugin installation");
|
|
8736
|
+
}
|
|
8599
8737
|
ensurePluginsDir();
|
|
8600
8738
|
const gitignoreResult = await ensureGitignore();
|
|
8601
8739
|
if (gitignoreResult.modified) {
|